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.
@@ -1,31 +1,60 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Author:: Steven Murawski (<steven.murawski@gmail.com>)
2
4
  #
3
- # Copyright (C) 2015, Steven Murawski
5
+ # Copyright (c) 2015 Steven Murawski
4
6
  #
5
- # Licensed under the Apache License, Version 2.0 (the "License");
6
- # you may not use this file except in compliance with the License.
7
- # You may obtain a copy of the License at
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
- # http://www.apache.org/licenses/LICENSE-2.0
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
- # Unless required by applicable law or agreed to in writing, software
12
- # distributed under the License is distributed on an "AS IS" BASIS,
13
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
- # See the License for the specific language governing permissions and
15
- # limitations under the License.
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
- destination = destination.gsub("%{instance_name}", instance.name)
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?('^/|^[a-zA-Z]:[\\/]') # is Absolute?
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?('\\$|/$') # is Folder (ends with / or \)
201
- destination = File.join(destination, File.basename(source)).to_s
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
- # Download functionality was added to the base verifier behavior after
216
- # version 2.3.4
217
- if Gem::Version.new(Kitchen::VERSION) <= Gem::Version.new("2.3.4")
218
- def call(state)
219
- super
220
- ensure
221
- info("Ensure download test files.")
222
- download_test_files(state) unless config[:downloads].nil?
223
- info("Download complete.")
224
- end
225
- else
226
- def call(state)
227
- super
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
- # private
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
- Array(bootstrap[:modules]).map do |powershell_module|
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
- ${#{powershell_module[:Name]}} = #{ps_hash(powershell_module)}
386
+ ${#{module_name}} = #{ps_hash(powershell_module)}
334
387
 
335
- Install-ModuleFromNuget -Module ${#{powershell_module[:Name]}} #{gallery_url_param}
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
- Array(config[:register_repository]).map do |psrepo|
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 #{psrepo[:Name]}.")
410
+ debug("Command to set PSRepo #{repo_name}.")
357
411
  <<-PS1
358
- Write-Host 'Registering psrepo #{psrepo[:Name]}...'
359
- ${#{psrepo[:Name]}} = #{ps_hash(psrepo)}
360
- Set-PSRepo -Repository ${#{psrepo[:Name]}}
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 <String> command to install Pester Module
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 sepcified in install_modules config.
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
- Array(config[:install_modules]).map do |powershell_module|
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[:Name].gsub(/[\W]/, "_")
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
- # Get the defined shell or fall back to pwsh, unless we're on windows where we use powershell
422
- # call via sudo if sudo is true.
423
- # This allows to use pwsh-preview instead of pwsh, or a full path to a specific binary.
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
- # Writing the command to a ps1 file, adding the pwsh shebang
460
- # invoke the file
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 verifier.windows_os?
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
- # Creates a PowerShell hashtable from a ruby map.
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
- "'" + obj.to_s + "'"
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} = '#{v}'"
756
+ "$env:#{k} = #{ps_single_quote(v)}"
630
757
  end
631
758
 
632
759
  commands.join("\n")
633
760
  end
634
761
 
635
- # returns the path of the modules subfolder
636
- # in the sandbox, where PS Modules and folders will be copied to.
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
- # copy files into the 'modules' folder of the sandbox,
644
- # so that copied folders can be discovered with the updated $Env:PSModulePath.
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
- # returns an array of string
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
- # @return [Array<String>] array of files in a folder
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
- # mostly used to copy to the sandbox.
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
- # returns the absolute path of the folders containing the
707
- # test suites, use default if not set.
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
- # returns the absolute path of the relative folders containing the
715
- # test suites, use default i not set.
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
- # returns a string of space of the specified depth.
725
- # This is used to pad messages or when building PS hashtables.
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
- PESTER_VERSION = "1.2.0".freeze
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