lib/kitchen/verifier/pester.rb in kitchen-pester-1.0.0 vs lib/kitchen/verifier/pester.rb in kitchen-pester-1.1.0

- old
+ new

@@ -12,16 +12,16 @@ # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -require "fileutils" -require "pathname" +require "fileutils" unless defined?(FileUtils) +require "pathname" unless defined?(Pathname) require "kitchen/util" require "kitchen/verifier/base" require "kitchen/version" -require "base64" +require "base64" unless defined?(Base64) require_relative "pester_version" module Kitchen module Verifier @@ -45,14 +45,29 @@ default_config :pester_install, { SkipPublisherCheck: true, Force: true, ErrorAction: "Stop", } + default_config :pester_configuration, { + run: { + path: ".", + PassThru: true, + }, + TestResult: { + Enabled: true, + OutputPath: "PesterTestResults.xml", + TestSuiteName: "", + }, + Output: { + Verbosity: "Detailed", + }, + } default_config :install_modules, [] - default_config :downloads, ["./PesterTestResults.xml"] => "./testresults" + default_config :downloads, { "./PesterTestResults.xml" => "./testresults/" } default_config :copy_folders, [] default_config :sudo, false + default_config :shell, nil # Creates a new Verifier object using the provided configuration data # which will be merged with any default configuration. # # @param config [Hash] provided verifier configuration @@ -113,11 +128,11 @@ Get-module -ListAvailable -FullyQualifiedName @{ModuleName = 'Pester'; RequiredVersion = '3.4.0'} } ) if ($modulesToRemove.ModuleBase.Count -eq 0) { - # for PS7 on linux + # for PS7 on linux return } $modulesToRemove.ModuleBase | Foreach-Object { $ModuleBaseLeaf = Split-Path -Path $_ -Leaf @@ -148,56 +163,152 @@ # required, then `nil` will be returned. # # @return [String] a command string def prepare_command info("Preparing the SUT and Pester dependencies...") + resolve_downloads_paths! really_wrap_shell_code(install_command_script) end # 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. # # @return [String] a command string def run_command - really_wrap_shell_code(run_command_script) + really_wrap_shell_code(invoke_pester_scriptblock) end + # Resolves the remote Downloads path from the verifier root path, + # unless they're absolute path (starts with / or C:\) + # This updates the config[:downloads], nothing (nil) is returned. + # + # @return [nil] updates config downloads + def resolve_downloads_paths! + info("Resolving Downloads path from config.") + config[:downloads] = config[:downloads] + .map do |source, destination| + source = source.to_s + info(" resolving remote source's absolute path.") + unless source.match?('^/|^[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?('\\$|/$') # is Folder (ends with / or \) + destination = File.join(destination, File.basename(source)).to_s + end + info(" Destination: #{destination}") + if !File.directory?(File.dirname(destination)) + FileUtils.mkdir_p(File.dirname(destination)) + else + info(" Directory #{File.dirname(destination)} seem to exist.") + end + + [ source, destination ] + end + nil # make sure we do not return anything + end + # Download functionality was added to the base verifier behavior after # version 2.3.4 if Gem::Version.new(Kitchen::VERSION) <= Gem::Version.new("2.3.4") def call(state) super ensure - download_test_files(state) unless config[:download].nil? + info("Ensure download test files.") + download_test_files(state) unless config[:downloads].nil? + info("Download complete.") end else def call(state) super rescue # If the verifier reports failure, we need to download the files ourselves. # Test Kitchen's base verifier doesn't have the download in an `ensure` block. - download_test_files(state) unless config[:download].nil? - + info("Rescue to download test files.") + download_test_files(state) unless config[:downloads].nil? # Rethrow original exception, we still want to register the failure. raise end end # private - def run_command_script + def invoke_pester_scriptblock <<-PS1 - Import-Module -Name Pester -Force -ErrorAction Stop + $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' - $options = New-PesterOption -TestSuiteName "Pester - #{instance.to_str}" + 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 + } - $result = Invoke-Pester -Script $TestPath -OutputFile $OutputFilePath -OutputFormat NUnitXml -PesterOption $options -PassThru - $result | Export-CliXml -Path (Join-Path -Path $TestPath -ChildPath 'result.xml') + $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 + } + $LASTEXITCODE = $result.FailedCount $host.SetShouldExit($LASTEXITCODE) exit $LASTEXITCODE PS1 @@ -214,10 +325,11 @@ 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} @@ -229,11 +341,11 @@ # Returns the string command to set a PS Repository # for each PSRepo configured. # # @return [Array<String>] array of suite files # @api private - def register_psrepository + 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 @@ -300,59 +412,79 @@ def really_wrap_shell_code(code) windows_os? ? really_wrap_windows_shell_code(code) : really_wrap_posix_shell_code(code) end + # Get the defined shell or fall back to pwsh, unless we're on windows where we use powershell + # call via sudo if sudo is true. + # This allows to use pwsh-preview instead of pwsh, or a full path to a specific binary. + 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 + def really_wrap_windows_shell_code(code) - wrap_shell_code(Util.outdent!(use_local_powershell_modules(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 { + Set-ExecutionPolicy Unrestricted -force + } + 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 # Writing the command to a ps1 file, adding the pwsh shebang # invoke the file 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 + # 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 - # 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 + 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 def use_local_powershell_modules(script) <<-PS1 - try { - if (!$IsLinux -and !$IsMacOs) { - Set-ExecutionPolicy Unrestricted -force - } - } - catch { - $_ | Out-String | Write-Warning - } - + 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 un Linux, it will run as root (via sudo). + # 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} @@ -365,11 +497,11 @@ 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?} + #{register_psrepository_scriptblock.join("\n") unless config[:register_repository].nil?} #{install_pester} #{install_modules_from_gallery.join("\n") unless config[:install_modules].nil?} PS1 @@ -387,16 +519,19 @@ CMD )) end def download_test_files(state) - return if config[:downloads].nil? + 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].to_h.each do |remotes, local| - debug("Downloading #{Array(remotes).join(", ")} to #{local}") + 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}") @@ -525,11 +660,11 @@ sandboxed_suites_path = File.join(sandbox_path, "suites") copy_if_src_exists(suite_test_folder, sandboxed_suites_path) end def prepare_supporting_psmodules - debug("Preparing to copy files from '#{support_psmodule_folder}' to the SUT.") + 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 # Copies a folder recursively preserving its layers, @@ -540,10 +675,10 @@ 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}") + 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"))