kitchen-pester 1.2.0 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +443 -0
- data/kitchen-pester.gemspec +11 -8
- data/lib/kitchen/verifier/pester.rb +308 -81
- data/lib/kitchen/verifier/pester_version.rb +31 -1
- data/lib/support/modules/PesterUtil/PesterUtil.psm1 +20 -0
- metadata +13 -62
- data/Gemfile +0 -29
- data/Rakefile +0 -57
|
@@ -1,31 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
# Author:: Steven Murawski (<steven.murawski@gmail.com>)
|
|
2
4
|
#
|
|
3
|
-
# Copyright (
|
|
5
|
+
# Copyright (c) 2015 Steven Murawski
|
|
4
6
|
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
7
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
9
|
+
# in the Software without restriction, including without limitation the rights
|
|
10
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
# furnished to do so, subject to the following conditions:
|
|
8
13
|
#
|
|
9
|
-
#
|
|
14
|
+
# The above copyright notice and this permission notice shall be included in
|
|
15
|
+
# all copies or substantial portions of the Software.
|
|
10
16
|
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
17
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
23
|
+
# THE SOFTWARE.
|
|
16
24
|
|
|
17
25
|
require "fileutils" unless defined?(FileUtils)
|
|
18
26
|
require "pathname" unless defined?(Pathname)
|
|
19
27
|
require "kitchen/util"
|
|
20
28
|
require "kitchen/verifier/base"
|
|
21
|
-
require "kitchen/version"
|
|
22
|
-
require "base64" unless defined?(Base64)
|
|
23
29
|
require_relative "pester_version"
|
|
24
30
|
|
|
25
31
|
module Kitchen
|
|
26
32
|
|
|
27
33
|
module Verifier
|
|
28
34
|
|
|
35
|
+
# A Test Kitchen verifier that runs Pester tests on the system under test.
|
|
36
|
+
#
|
|
37
|
+
# The verifier does almost all of its work by generating PowerShell source
|
|
38
|
+
# locally and handing it to the transport to execute remotely. Each command
|
|
39
|
+
# hook -- {#install_command}, {#init_command}, {#prepare_command} and
|
|
40
|
+
# {#run_command} -- returns a script string rather than performing the work
|
|
41
|
+
# itself.
|
|
42
|
+
#
|
|
43
|
+
# Test files, helper files and any folders named in `copy_folders` are
|
|
44
|
+
# staged into a sandbox by {#create_sandbox}, shipped to the instance, and
|
|
45
|
+
# discovered there through `$Env:PSModulePath`.
|
|
46
|
+
#
|
|
47
|
+
# @example configuring the verifier in kitchen.yml
|
|
48
|
+
#
|
|
49
|
+
# verifier:
|
|
50
|
+
# name: pester
|
|
51
|
+
# test_folder: tests
|
|
52
|
+
# install_modules:
|
|
53
|
+
# - PSScriptAnalyzer
|
|
54
|
+
# downloads:
|
|
55
|
+
# ./PesterTestResults.xml: ./testresults/
|
|
56
|
+
#
|
|
57
|
+
# @see https://pester.dev/ Pester
|
|
29
58
|
class Pester < Kitchen::Verifier::Base
|
|
30
59
|
|
|
31
60
|
kitchen_verifier_api_version 1
|
|
@@ -95,6 +124,8 @@ module Kitchen
|
|
|
95
124
|
# # any further file copies, preparations, etc.
|
|
96
125
|
# end
|
|
97
126
|
# end
|
|
127
|
+
#
|
|
128
|
+
# @return [void]
|
|
98
129
|
def create_sandbox
|
|
99
130
|
super
|
|
100
131
|
prepare_supporting_psmodules
|
|
@@ -190,15 +221,24 @@ module Kitchen
|
|
|
190
221
|
config[:downloads] = config[:downloads]
|
|
191
222
|
.map do |source, destination|
|
|
192
223
|
source = source.to_s
|
|
193
|
-
|
|
224
|
+
if destination.nil?
|
|
225
|
+
raise UserError, "The verifier's 'downloads' entry for '#{source}' has no local " \
|
|
226
|
+
"destination. Every entry needs one, for example " \
|
|
227
|
+
"'#{source}: ./testresults/'."
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
destination = destination.to_s.gsub("%{instance_name}", instance.name)
|
|
194
231
|
info(" resolving remote source's absolute path.")
|
|
195
|
-
unless source.match?(
|
|
232
|
+
unless source.match?(%r{^/|^[a-zA-Z]:[\\/]}) # is Absolute?
|
|
196
233
|
info(" '#{source}' is a relative path, resolving to: #{File.join(config[:root_path], source)}")
|
|
197
234
|
source = File.join(config[:root_path], source.to_s).to_s
|
|
198
235
|
end
|
|
199
236
|
|
|
200
|
-
if destination.match?(
|
|
201
|
-
|
|
237
|
+
if destination.match?(%r{[\\/]$}) # is Folder (ends with / or \)
|
|
238
|
+
# Append to the separator the user already supplied. File.join
|
|
239
|
+
# would add a second one, of whichever flavour the workstation
|
|
240
|
+
# happens to use.
|
|
241
|
+
destination = "#{destination}#{remote_basename(source)}"
|
|
202
242
|
end
|
|
203
243
|
info(" Destination: #{destination}")
|
|
204
244
|
if !File.directory?(File.dirname(destination))
|
|
@@ -209,33 +249,34 @@ module Kitchen
|
|
|
209
249
|
|
|
210
250
|
[ source, destination ]
|
|
211
251
|
end
|
|
252
|
+
.to_h # Hash#map yields pairs; keep :downloads the hash it started as
|
|
212
253
|
nil # make sure we do not return anything
|
|
213
254
|
end
|
|
214
255
|
|
|
215
|
-
#
|
|
216
|
-
#
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
rescue
|
|
229
|
-
# If the verifier reports failure, we need to download the files ourselves.
|
|
230
|
-
# Test Kitchen's base verifier doesn't have the download in an `ensure` block.
|
|
231
|
-
info("Rescue to download test files.")
|
|
232
|
-
download_test_files(state) unless config[:downloads].nil?
|
|
233
|
-
# Rethrow original exception, we still want to register the failure.
|
|
234
|
-
raise
|
|
235
|
-
end
|
|
256
|
+
# Runs the verifier on the instance, retrieving the test results even
|
|
257
|
+
# when the run fails.
|
|
258
|
+
#
|
|
259
|
+
# @param state [Hash] mutable instance state
|
|
260
|
+
# @raise [Kitchen::ActionFailed] if the verification failed
|
|
261
|
+
# @return [void]
|
|
262
|
+
def call(state)
|
|
263
|
+
super
|
|
264
|
+
rescue
|
|
265
|
+
info("Rescue to download test files.")
|
|
266
|
+
download_test_files(state) unless config[:downloads].nil?
|
|
267
|
+
# Rethrow the original exception; the failure still has to register.
|
|
268
|
+
raise
|
|
236
269
|
end
|
|
237
270
|
|
|
238
|
-
#
|
|
271
|
+
# Returns the PowerShell that imports Pester and invokes it.
|
|
272
|
+
#
|
|
273
|
+
# Two dialects are emitted behind a version check evaluated on the SUT:
|
|
274
|
+
# Pester 4 and earlier take loose parameters, Pester 5 and later take a
|
|
275
|
+
# `PesterConfiguration` object. The script exits with Pester's failed
|
|
276
|
+
# test count so the transport registers the failure.
|
|
277
|
+
#
|
|
278
|
+
# @return [String] a PowerShell script
|
|
279
|
+
# @api private
|
|
239
280
|
def invoke_pester_scriptblock
|
|
240
281
|
<<-PS1
|
|
241
282
|
$PesterModule = Import-Module -Name Pester -Force -ErrorAction Stop -PassThru
|
|
@@ -308,7 +349,7 @@ module Kitchen
|
|
|
308
349
|
|
|
309
350
|
$resultXmlPath = (Join-Path -Path $TestPath -ChildPath 'result.xml')
|
|
310
351
|
if (Test-Path -Path $resultXmlPath) {
|
|
311
|
-
$result | Export-CliXml -Path
|
|
352
|
+
$result | Export-CliXml -Path $resultXmlPath
|
|
312
353
|
}
|
|
313
354
|
|
|
314
355
|
$LASTEXITCODE = $result.FailedCount
|
|
@@ -318,6 +359,17 @@ module Kitchen
|
|
|
318
359
|
PS1
|
|
319
360
|
end
|
|
320
361
|
|
|
362
|
+
# Returns the commands that install the bootstrap modules straight from
|
|
363
|
+
# a NuGet feed.
|
|
364
|
+
#
|
|
365
|
+
# This runs before PowerShellGet is available, so it uses
|
|
366
|
+
# `Install-ModuleFromNuget` from PesterUtil.psm1 rather than
|
|
367
|
+
# `Install-Module`. Each entry of `bootstrap.modules` may be a plain
|
|
368
|
+
# module name or a hash of parameters.
|
|
369
|
+
#
|
|
370
|
+
# @return [Array<String>, nil] one PowerShell fragment per module, or nil
|
|
371
|
+
# when no bootstrap modules are configured
|
|
372
|
+
# @api private
|
|
321
373
|
def get_powershell_modules_from_nugetapi
|
|
322
374
|
# don't return anything is the modules subkey or bootstrap is null
|
|
323
375
|
return if config.dig(:bootstrap, :modules).nil?
|
|
@@ -327,12 +379,13 @@ module Kitchen
|
|
|
327
379
|
gallery_url_param = bootstrap[:repository_url] ? "-GalleryUrl '#{bootstrap[:repository_url]}'" : ""
|
|
328
380
|
|
|
329
381
|
info("Bootstrapping environment without PowerShellGet Provider...")
|
|
330
|
-
|
|
382
|
+
config_list("bootstrap.modules", bootstrap[:modules]).map do |powershell_module|
|
|
331
383
|
if powershell_module.is_a? Hash
|
|
384
|
+
module_name = module_name!("bootstrap.modules", powershell_module)
|
|
332
385
|
<<-PS1
|
|
333
|
-
${#{
|
|
386
|
+
${#{module_name}} = #{ps_hash(powershell_module)}
|
|
334
387
|
|
|
335
|
-
Install-ModuleFromNuget -Module ${#{
|
|
388
|
+
Install-ModuleFromNuget -Module ${#{module_name}} #{gallery_url_param}
|
|
336
389
|
PS1
|
|
337
390
|
else
|
|
338
391
|
<<-PS1
|
|
@@ -351,13 +404,14 @@ module Kitchen
|
|
|
351
404
|
return if config[:register_repository].nil?
|
|
352
405
|
|
|
353
406
|
info("Registering a new PowerShellGet Repository")
|
|
354
|
-
|
|
407
|
+
config_list("register_repository", config[:register_repository]).map do |psrepo|
|
|
408
|
+
repo_name = module_name!("register_repository", psrepo)
|
|
355
409
|
# Using Set-PSRepo from ../../*/*/*/PesterUtil.psm1
|
|
356
|
-
debug("Command to set PSRepo #{
|
|
410
|
+
debug("Command to set PSRepo #{repo_name}.")
|
|
357
411
|
<<-PS1
|
|
358
|
-
Write-Host 'Registering psrepo #{
|
|
359
|
-
${#{
|
|
360
|
-
Set-PSRepo -Repository ${#{
|
|
412
|
+
Write-Host 'Registering psrepo #{repo_name}...'
|
|
413
|
+
${#{repo_name}} = #{ps_hash(psrepo)}
|
|
414
|
+
Set-PSRepo -Repository ${#{repo_name}}
|
|
361
415
|
PS1
|
|
362
416
|
end
|
|
363
417
|
end
|
|
@@ -365,7 +419,7 @@ module Kitchen
|
|
|
365
419
|
# Returns the string command set the PSGallery as trusted, and
|
|
366
420
|
# Install Pester from gallery based on the params from Pester_install_params config
|
|
367
421
|
#
|
|
368
|
-
# @return
|
|
422
|
+
# @return [String] command to install Pester Module
|
|
369
423
|
# @api private
|
|
370
424
|
def install_pester
|
|
371
425
|
return if config[:skip_pester_install]
|
|
@@ -386,17 +440,17 @@ module Kitchen
|
|
|
386
440
|
end
|
|
387
441
|
|
|
388
442
|
# returns a piece of PS scriptblock for each Module to install
|
|
389
|
-
# from gallery that has been
|
|
443
|
+
# from gallery that has been specified in install_modules config.
|
|
390
444
|
#
|
|
391
445
|
# @return [Array<String>] array of PS commands.
|
|
392
446
|
# @api private
|
|
393
447
|
def install_modules_from_gallery
|
|
394
448
|
return if config[:install_modules].nil?
|
|
395
449
|
|
|
396
|
-
|
|
450
|
+
config_list("install_modules", config[:install_modules]).map do |powershell_module|
|
|
397
451
|
if powershell_module.is_a? Hash
|
|
398
452
|
# Sanitize variable name so that $powershell-yaml becomes $powershell_yaml
|
|
399
|
-
module_name = powershell_module
|
|
453
|
+
module_name = module_name!("install_modules", powershell_module).gsub(/[\W]/, "_")
|
|
400
454
|
# so we can splat that variable to install module
|
|
401
455
|
<<-PS1
|
|
402
456
|
$#{module_name} = #{ps_hash(powershell_module)}
|
|
@@ -414,13 +468,31 @@ module Kitchen
|
|
|
414
468
|
end
|
|
415
469
|
end
|
|
416
470
|
|
|
471
|
+
# Note for anyone adding to the script builders below: Kitchen::Util.outdent!
|
|
472
|
+
# mutates the string it is handed, and this file sets
|
|
473
|
+
# `frozen_string_literal: true`. Interpolated literals are not frozen, so
|
|
474
|
+
# every heredoc that reaches outdent! today is fine -- but a heredoc with
|
|
475
|
+
# no `#{}` in it would be frozen and would raise FrozenError at runtime.
|
|
476
|
+
# Use +dup+ on any such string before passing it along.
|
|
477
|
+
|
|
478
|
+
# Wraps generated PowerShell in the platform's shell invocation.
|
|
479
|
+
#
|
|
480
|
+
# @param code [String] the PowerShell to run on the instance
|
|
481
|
+
# @return [String] a shell command string
|
|
482
|
+
# @api private
|
|
417
483
|
def really_wrap_shell_code(code)
|
|
418
484
|
windows_os? ? really_wrap_windows_shell_code(code) : really_wrap_posix_shell_code(code)
|
|
419
485
|
end
|
|
420
486
|
|
|
421
|
-
#
|
|
422
|
-
#
|
|
423
|
-
#
|
|
487
|
+
# Returns the shell binary used to run the generated script.
|
|
488
|
+
#
|
|
489
|
+
# An explicit `shell` config wins, which allows pwsh-preview or a full
|
|
490
|
+
# path to a specific binary. Otherwise Windows uses powershell and every
|
|
491
|
+
# other platform uses pwsh. `sudo` is honoured everywhere except the
|
|
492
|
+
# Windows branch, where it is meaningless.
|
|
493
|
+
#
|
|
494
|
+
# @return [String] the shell command, prefixed with sudo when configured
|
|
495
|
+
# @api private
|
|
424
496
|
def shell_cmd
|
|
425
497
|
if !config[:shell].nil?
|
|
426
498
|
config[:sudo] ? "sudo #{config[:shell]}" : "#{config[:shell]}"
|
|
@@ -431,6 +503,15 @@ module Kitchen
|
|
|
431
503
|
end
|
|
432
504
|
end
|
|
433
505
|
|
|
506
|
+
# Wraps PowerShell for a Windows instance.
|
|
507
|
+
#
|
|
508
|
+
# The payload is written to kitchen_cmd.ps1 and invoked, rather than
|
|
509
|
+
# passed on the command line, so that quoting and length limits do not
|
|
510
|
+
# apply to it.
|
|
511
|
+
#
|
|
512
|
+
# @param code [String] the PowerShell to run on the instance
|
|
513
|
+
# @return [String] a shell command string
|
|
514
|
+
# @api private
|
|
434
515
|
def really_wrap_windows_shell_code(code)
|
|
435
516
|
my_command = <<-PWSH
|
|
436
517
|
echo "Running as '$(whoami)'..."
|
|
@@ -456,8 +537,15 @@ module Kitchen
|
|
|
456
537
|
wrap_shell_code(Util.outdent!(my_command))
|
|
457
538
|
end
|
|
458
539
|
|
|
459
|
-
#
|
|
460
|
-
#
|
|
540
|
+
# Wraps PowerShell for a non-Windows instance.
|
|
541
|
+
#
|
|
542
|
+
# Writes the payload to kitchen_cmd.ps1 through a quoted heredoc, so the
|
|
543
|
+
# POSIX shell does not interpolate PowerShell variables, adds a pwsh
|
|
544
|
+
# shebang and invokes it.
|
|
545
|
+
#
|
|
546
|
+
# @param code [String] the PowerShell to run on the instance
|
|
547
|
+
# @return [String] a shell command string
|
|
548
|
+
# @api private
|
|
461
549
|
def really_wrap_posix_shell_code(code)
|
|
462
550
|
my_command = <<-BASH
|
|
463
551
|
echo "Running as '$(whoami)'"
|
|
@@ -478,6 +566,12 @@ module Kitchen
|
|
|
478
566
|
Util.outdent!(my_command)
|
|
479
567
|
end
|
|
480
568
|
|
|
569
|
+
# Prefixes a script with the preamble that makes the sandbox's modules
|
|
570
|
+
# folder importable.
|
|
571
|
+
#
|
|
572
|
+
# @param script [String] the PowerShell to run after the preamble
|
|
573
|
+
# @return [String] the script with the PSModulePath preamble prepended
|
|
574
|
+
# @api private
|
|
481
575
|
def use_local_powershell_modules(script)
|
|
482
576
|
<<-PS1
|
|
483
577
|
Write-Host -Object ("{0} - PowerShell {1}" -f $PSVersionTable.OS,$PSVersionTable.PSVersion)
|
|
@@ -497,6 +591,16 @@ module Kitchen
|
|
|
497
591
|
PS1
|
|
498
592
|
end
|
|
499
593
|
|
|
594
|
+
# Returns the PowerShell that prepares the SUT once the sandbox has been
|
|
595
|
+
# transferred.
|
|
596
|
+
#
|
|
597
|
+
# Runs after the transfer so that PesterUtil.psm1 is available to import.
|
|
598
|
+
# Composes, in order: the NuGet bootstrap, any PSRepository registration,
|
|
599
|
+
# the Pester install, and any gallery modules. Each section is omitted
|
|
600
|
+
# when its config is nil.
|
|
601
|
+
#
|
|
602
|
+
# @return [String] a PowerShell script
|
|
603
|
+
# @api private
|
|
500
604
|
def install_command_script
|
|
501
605
|
<<-PS1
|
|
502
606
|
$PSModPathToPrepend = "#{config[:root_path]}"
|
|
@@ -513,8 +617,16 @@ module Kitchen
|
|
|
513
617
|
PS1
|
|
514
618
|
end
|
|
515
619
|
|
|
620
|
+
# Returns the command that schedules and runs a WinRM restart.
|
|
621
|
+
#
|
|
622
|
+
# The restart is driven through a scheduled task so that it survives the
|
|
623
|
+
# WinRM session being torn down by the restart itself.
|
|
624
|
+
#
|
|
625
|
+
# @return [String, nil] a shell command string, or nil on a non-Windows
|
|
626
|
+
# instance
|
|
627
|
+
# @api private
|
|
516
628
|
def restart_winrm_service
|
|
517
|
-
return unless
|
|
629
|
+
return unless windows_os?
|
|
518
630
|
|
|
519
631
|
cmd = "schtasks /Create /TN restart_winrm /TR " \
|
|
520
632
|
'"powershell -Command Restart-Service winrm" ' \
|
|
@@ -526,6 +638,12 @@ module Kitchen
|
|
|
526
638
|
))
|
|
527
639
|
end
|
|
528
640
|
|
|
641
|
+
# Retrieves the configured result files from the instance.
|
|
642
|
+
#
|
|
643
|
+
# @param state [Hash] mutable instance state, used to open the transport
|
|
644
|
+
# connection
|
|
645
|
+
# @return [void]
|
|
646
|
+
# @api private
|
|
529
647
|
def download_test_files(state)
|
|
530
648
|
if config[:downloads].nil?
|
|
531
649
|
info("Skipped downloading test result file from #{instance.to_str}; 'downloads' hash is empty.")
|
|
@@ -581,8 +699,9 @@ module Kitchen
|
|
|
581
699
|
end
|
|
582
700
|
|
|
583
701
|
# Copies all common testing helper files into the suites directory in
|
|
584
|
-
# the sandbox.
|
|
702
|
+
# the sandbox, stripping the `helpers/` prefix from their paths.
|
|
585
703
|
#
|
|
704
|
+
# @return [void]
|
|
586
705
|
# @api private
|
|
587
706
|
def prepare_helpers
|
|
588
707
|
base = File.join(test_folder, "helpers")
|
|
@@ -595,9 +714,15 @@ module Kitchen
|
|
|
595
714
|
end
|
|
596
715
|
end
|
|
597
716
|
|
|
598
|
-
#
|
|
599
|
-
# The only types supported for now are hash, array, string and Boolean.
|
|
717
|
+
# Renders a Ruby value as PowerShell source.
|
|
600
718
|
#
|
|
719
|
+
# Hashes become hashtables, arrays become arrays, booleans become $true
|
|
720
|
+
# or $false, and everything else is quoted as a string -- PowerShell is
|
|
721
|
+
# generally able to coerce it back to the type it needs.
|
|
722
|
+
#
|
|
723
|
+
# @param obj [Object] the value to render
|
|
724
|
+
# @param depth [Integer] current nesting depth, used for indentation
|
|
725
|
+
# @return [String] PowerShell source for the value
|
|
601
726
|
# @api private
|
|
602
727
|
def ps_hash(obj, depth = 0)
|
|
603
728
|
if [true, false].include? obj
|
|
@@ -617,51 +742,71 @@ module Kitchen
|
|
|
617
742
|
else
|
|
618
743
|
# When the object is not a string nor a hash or array, it will be quoted as a string.
|
|
619
744
|
# In most cases, PS is smart enough to convert back to the type it needs.
|
|
620
|
-
|
|
745
|
+
ps_single_quote(obj)
|
|
621
746
|
end
|
|
622
747
|
end
|
|
623
748
|
|
|
624
749
|
# Creates environment variable assignments from a ruby map.
|
|
625
750
|
#
|
|
751
|
+
# @param obj [Hash] variable names mapped to their values
|
|
752
|
+
# @return [String] newline-separated `$env:NAME = 'value'` assignments
|
|
626
753
|
# @api private
|
|
627
754
|
def ps_environment(obj)
|
|
628
755
|
commands = obj.map do |k, v|
|
|
629
|
-
"$env:#{k} =
|
|
756
|
+
"$env:#{k} = #{ps_single_quote(v)}"
|
|
630
757
|
end
|
|
631
758
|
|
|
632
759
|
commands.join("\n")
|
|
633
760
|
end
|
|
634
761
|
|
|
635
|
-
#
|
|
636
|
-
#
|
|
762
|
+
# Renders a value as a single-quoted PowerShell string literal.
|
|
763
|
+
#
|
|
764
|
+
# PowerShell escapes a literal quote inside a single-quoted string by
|
|
765
|
+
# doubling it. Without this an apostrophe anywhere in the config -- a
|
|
766
|
+
# module name, an environment value, a password -- closes the string
|
|
767
|
+
# early and corrupts the rest of the generated script.
|
|
768
|
+
#
|
|
769
|
+
# @param value [Object] any value; #to_s is used
|
|
770
|
+
# @return [String] a quoted, escaped PowerShell string literal
|
|
771
|
+
# @api private
|
|
772
|
+
def ps_single_quote(value)
|
|
773
|
+
"'#{value.to_s.gsub("'", "''")}'"
|
|
774
|
+
end
|
|
775
|
+
|
|
776
|
+
# Returns the path of the modules subfolder in the sandbox, where PS
|
|
777
|
+
# modules and folders will be copied to.
|
|
637
778
|
#
|
|
779
|
+
# @return [String] absolute path to the sandbox's modules folder
|
|
638
780
|
# @api private
|
|
639
781
|
def sandbox_module_path
|
|
640
782
|
File.join(sandbox_path, "modules")
|
|
641
783
|
end
|
|
642
784
|
|
|
643
|
-
#
|
|
644
|
-
# so
|
|
785
|
+
# Copies the folders named in `copy_folders` into the sandbox's modules
|
|
786
|
+
# folder, so they can be discovered through the updated
|
|
787
|
+
# $Env:PSModulePath.
|
|
645
788
|
#
|
|
789
|
+
# @return [void]
|
|
646
790
|
# @api private
|
|
647
791
|
def prepare_copy_folders
|
|
648
792
|
return if config[:copy_folders].nil?
|
|
649
793
|
|
|
650
794
|
info("Preparing to copy specified folders to #{sandbox_module_path}.")
|
|
651
795
|
kitchen_root_path = config[:kitchen_root]
|
|
652
|
-
config[:copy_folders].each do |folder|
|
|
796
|
+
config_list("copy_folders", config[:copy_folders]).each do |folder|
|
|
653
797
|
debug("copying #{folder}")
|
|
654
798
|
folder_to_copy = File.join(kitchen_root_path, folder)
|
|
655
799
|
copy_if_src_exists(folder_to_copy, sandbox_module_path)
|
|
656
800
|
end
|
|
657
801
|
end
|
|
658
802
|
|
|
659
|
-
#
|
|
660
|
-
# Creates a flat list of files contained in a folder.
|
|
661
|
-
# This is useful when trying to debug what has been copied to
|
|
662
|
-
# the sandbox.
|
|
803
|
+
# Creates a flat list of the files contained in a folder.
|
|
663
804
|
#
|
|
664
|
-
#
|
|
805
|
+
# Useful when debugging what has actually been copied to the sandbox.
|
|
806
|
+
#
|
|
807
|
+
# @param path [String] the folder to list
|
|
808
|
+
# @return [Array<String>] paths of the entries at the top level and
|
|
809
|
+
# nested beneath it
|
|
665
810
|
# @api private
|
|
666
811
|
def list_files(path)
|
|
667
812
|
base_directory_content = Dir.glob(File.join(path, "*"))
|
|
@@ -671,6 +816,7 @@ module Kitchen
|
|
|
671
816
|
|
|
672
817
|
# Copies all test suite files into the suites directory in the sandbox.
|
|
673
818
|
#
|
|
819
|
+
# @return [void]
|
|
674
820
|
# @api private
|
|
675
821
|
def prepare_pester_tests
|
|
676
822
|
info("Preparing to copy files from '#{suite_test_folder}' to the SUT.")
|
|
@@ -678,15 +824,23 @@ module Kitchen
|
|
|
678
824
|
copy_if_src_exists(suite_test_folder, sandboxed_suites_path)
|
|
679
825
|
end
|
|
680
826
|
|
|
827
|
+
# Copies PesterUtil.psm1 into the sandbox's modules folder, where the
|
|
828
|
+
# updated $Env:PSModulePath will find it.
|
|
829
|
+
#
|
|
830
|
+
# @return [void]
|
|
831
|
+
# @api private
|
|
681
832
|
def prepare_supporting_psmodules
|
|
682
833
|
info("Preparing to copy files from '#{support_psmodule_folder}' to the SUT.")
|
|
683
834
|
sandbox_module_path = File.join(sandbox_path, "modules")
|
|
684
835
|
copy_if_src_exists(support_psmodule_folder, sandbox_module_path)
|
|
685
836
|
end
|
|
686
837
|
|
|
687
|
-
# Copies a folder recursively preserving its layers
|
|
688
|
-
#
|
|
838
|
+
# Copies a folder recursively, preserving its layers. Mostly used to
|
|
839
|
+
# copy into the sandbox. Does nothing when the source does not exist.
|
|
689
840
|
#
|
|
841
|
+
# @param src_to_validate [String] folder to copy
|
|
842
|
+
# @param destination [String] folder to copy into, created if missing
|
|
843
|
+
# @return [void]
|
|
690
844
|
# @api private
|
|
691
845
|
def copy_if_src_exists(src_to_validate, destination)
|
|
692
846
|
unless Dir.exist?(src_to_validate)
|
|
@@ -703,27 +857,100 @@ module Kitchen
|
|
|
703
857
|
FileUtils.cp_r(src_to_validate, destination, preserve: true)
|
|
704
858
|
end
|
|
705
859
|
|
|
706
|
-
#
|
|
707
|
-
#
|
|
860
|
+
# Returns the folder containing the test suites, falling back to
|
|
861
|
+
# `test_base_path` when `test_folder` is not set.
|
|
708
862
|
#
|
|
863
|
+
# @return [String] path to the folder holding the suites
|
|
709
864
|
# @api private
|
|
710
865
|
def test_folder
|
|
711
866
|
config[:test_folder].nil? ? config[:test_base_path] : absolute_test_folder
|
|
712
867
|
end
|
|
713
868
|
|
|
714
|
-
#
|
|
715
|
-
#
|
|
869
|
+
# Resolves `test_folder` to an absolute path, descending into an
|
|
870
|
+
# `integration` subfolder when one exists.
|
|
716
871
|
#
|
|
872
|
+
# @return [String] absolute path to the folder holding the suites
|
|
717
873
|
# @api private
|
|
718
874
|
def absolute_test_folder
|
|
719
875
|
path = (Pathname.new config[:test_folder]).realpath
|
|
720
876
|
integration_path = File.join(path, "integration")
|
|
721
|
-
Dir.exist?(integration_path) ? integration_path : path
|
|
877
|
+
Dir.exist?(integration_path) ? integration_path : path.to_s
|
|
878
|
+
rescue Errno::ENOENT
|
|
879
|
+
raise UserError, "The verifier's 'test_folder' is set to " \
|
|
880
|
+
"'#{config[:test_folder]}', which does not exist. It is resolved " \
|
|
881
|
+
"relative to the directory kitchen runs in, so give it a path that " \
|
|
882
|
+
"exists there or an absolute one."
|
|
883
|
+
end
|
|
884
|
+
|
|
885
|
+
# Returns the entries of a config option that is documented as a list.
|
|
886
|
+
#
|
|
887
|
+
# YAML makes it easy to write a single mapping where a list of mappings
|
|
888
|
+
# was meant -- leaving off the leading `- ` is enough. `Array()` turns
|
|
889
|
+
# such a mapping into a list of `[key, value]` pairs, which then renders
|
|
890
|
+
# as nonsense PowerShell instead of failing, so reject it here where we
|
|
891
|
+
# can still say which option is at fault.
|
|
892
|
+
#
|
|
893
|
+
# @param key [String] the option's name, for the error message
|
|
894
|
+
# @param value [Object] the configured value
|
|
895
|
+
# @return [Array] the entries to iterate over
|
|
896
|
+
# @raise [Kitchen::UserError] when a single mapping was given
|
|
897
|
+
# @api private
|
|
898
|
+
def config_list(key, value)
|
|
899
|
+
if value.is_a?(Hash)
|
|
900
|
+
raise UserError, "The verifier's '#{key}' must be a list, but a single mapping " \
|
|
901
|
+
"was given. Put a '- ' in front of each entry in kitchen.yml."
|
|
902
|
+
end
|
|
903
|
+
|
|
904
|
+
Array(value)
|
|
905
|
+
end
|
|
906
|
+
|
|
907
|
+
# Returns the Name of a mapping-shaped entry in one of the module or
|
|
908
|
+
# repository lists.
|
|
909
|
+
#
|
|
910
|
+
# The name becomes a PowerShell variable that the generated script splats,
|
|
911
|
+
# so a missing one either blows up here or emits an empty `${}` that fails
|
|
912
|
+
# on the instance a long way from its cause.
|
|
913
|
+
#
|
|
914
|
+
# @param key [String] the option's name, for the error message
|
|
915
|
+
# @param entry [Hash] the entry to read the name from
|
|
916
|
+
# @return [String] the entry's Name
|
|
917
|
+
# @raise [Kitchen::UserError] when the entry is not a mapping, or has no
|
|
918
|
+
# usable Name
|
|
919
|
+
# @api private
|
|
920
|
+
def module_name!(key, entry)
|
|
921
|
+
unless entry.is_a?(Hash)
|
|
922
|
+
raise UserError, "Every entry under the verifier's '#{key}' must be a mapping with " \
|
|
923
|
+
"at least a 'Name'; #{entry.inspect} is a #{entry.class}."
|
|
924
|
+
end
|
|
925
|
+
|
|
926
|
+
name = entry[:Name] || entry["Name"]
|
|
927
|
+
if name.to_s.empty?
|
|
928
|
+
raise UserError, "Every mapping under the verifier's '#{key}' needs a 'Name'; " \
|
|
929
|
+
"#{entry.inspect} has none."
|
|
930
|
+
end
|
|
931
|
+
|
|
932
|
+
name.to_s
|
|
933
|
+
end
|
|
934
|
+
|
|
935
|
+
# Returns the final segment of a path that lives on the SUT.
|
|
936
|
+
#
|
|
937
|
+
# File.basename applies the *workstation's* separator rules, so a Windows
|
|
938
|
+
# remote path such as 'C:\results\out.xml' comes back unchanged when
|
|
939
|
+
# kitchen runs on macOS or Linux -- the usual case for a Windows SUT.
|
|
940
|
+
# Split on either separator instead.
|
|
941
|
+
#
|
|
942
|
+
# @param path [String] a path as it exists on the instance
|
|
943
|
+
# @return [String] the last path segment
|
|
944
|
+
# @api private
|
|
945
|
+
def remote_basename(path)
|
|
946
|
+
path.to_s.split(%r{[\\/]}).last.to_s
|
|
722
947
|
end
|
|
723
948
|
|
|
724
|
-
#
|
|
725
|
-
#
|
|
949
|
+
# Returns a run of spaces of the given width, used to pad messages and
|
|
950
|
+
# indent generated PowerShell hashtables.
|
|
726
951
|
#
|
|
952
|
+
# @param depth [Integer] number of spaces
|
|
953
|
+
# @return [String] the padding
|
|
727
954
|
# @api private
|
|
728
955
|
def pad(depth = 0)
|
|
729
956
|
" " * depth
|
|
@@ -1,5 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright (c) 2015 Steven Murawski
|
|
4
|
+
#
|
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
# furnished to do so, subject to the following conditions:
|
|
11
|
+
#
|
|
12
|
+
# The above copyright notice and this permission notice shall be included in
|
|
13
|
+
# all copies or substantial portions of the Software.
|
|
14
|
+
#
|
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
# THE SOFTWARE.
|
|
22
|
+
|
|
23
|
+
# Test Kitchen's top-level namespace.
|
|
1
24
|
module Kitchen
|
|
25
|
+
# Namespace for Test Kitchen verifier plugins.
|
|
2
26
|
module Verifier
|
|
3
|
-
|
|
27
|
+
# Version of the kitchen-pester gem.
|
|
28
|
+
#
|
|
29
|
+
# Kept in its own file so that the gemspec can read it without loading
|
|
30
|
+
# test-kitchen, which is not yet available when the gemspec is evaluated.
|
|
31
|
+
#
|
|
32
|
+
# @return [String] the gem version
|
|
33
|
+
PESTER_VERSION = "1.2.2"
|
|
4
34
|
end
|
|
5
35
|
end
|