# -*- encoding: utf-8 -*- # # Author:: Steven Murawski () # # Copyright (C) 2015, Steven Murawski # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # 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 "pathname" require "kitchen/verifier/base" require "kitchen/version" require_relative "pester_version" module Kitchen module Verifier class Pester < Kitchen::Verifier::Base kitchen_verifier_api_version 1 plugin_version Kitchen::Verifier::PESTER_VERSION default_config :restart_winrm, false default_config :test_folder default_config :use_local_pester_module, false default_config :downloads, ["./PesterTestResults.xml"] => "./testresults" # Creates a new Verifier object using the provided configuration data # which will be merged with any default configuration. # # @param config [Hash] provided verifier configuration def initialize(config = {}) init_config(config) end # 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: # # @example overriding `#create_sandbox` # # class MyVerifier < Kitchen::Verifier::Base # def create_sandbox # super # # any further file copies, preparations, etc. # end # end def create_sandbox super prepare_powershell_modules prepare_pester_tests prepare_helpers end # 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. # # @return [String] a command string def install_command return if local_suite_files.empty? return if config[:use_local_pester_module] really_wrap_shell_code(install_command_script) end # 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. # # @return [String] a command string def init_command restart_winrm_service if config[:restart_winrm] end # 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. # # @return [String] a command string def prepare_command; 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 return if local_suite_files.empty? really_wrap_shell_code(run_command_script) 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) 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) # Rethrow original exception, we still want to register the failure. raise end end # private def run_command_script <<-CMD Import-Module -Name Pester -Force $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 CMD end def really_wrap_shell_code(code) wrap_shell_code(Util.outdent!(use_local_powershell_modules(code))) end def use_local_powershell_modules(script) <<-EOH try { Set-ExecutionPolicy Unrestricted -force } catch { $_ | Out-String | Write-Warning } $global:ProgressPreference = 'SilentlyContinue' $env:PSModulePath = "$(Join-Path "#{config[:root_path]}" -ChildPath 'modules');$env:PSModulePath" #{script} EOH end def install_command_script <<-EOH [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 function Confirm-Directory { [CmdletBinding()] param($Path) $Item = if (Test-Path $Path) { Get-Item -Path $Path } else { New-Item -Path $Path -ItemType Directory } $Item.FullName } function Test-Module { [CmdletBinding()] param($Name) @(Get-Module -Name $Name -ListAvailable -ErrorAction SilentlyContinue).Count -gt 0 } $VerifierModulePath = Confirm-Directory -Path (Join-Path #{config[:root_path]} -ChildPath 'modules') $VerifierDownloadPath = Confirm-Directory -Path (Join-Path #{config[:root_path]} -ChildPath 'pester') $env:PSModulePath = "$VerifierModulePath;$PSModulePath" if (-not (Test-Module -Name Pester)) { if (Test-Module -Name PowerShellGet) { Import-Module PowerShellGet -Force Import-Module PackageManagement -Force Get-PackageProvider -Name NuGet -Force > $null Install-Module Pester -Force } else { if (-not (Test-Module -Name PsGet)){ $webClient = New-Object -TypeName System.Net.WebClient if ($env:HTTP_PROXY){ if ($env:NO_PROXY){ Write-Host "Creating WebProxy with 'HTTP_PROXY' and 'NO_PROXY' environment variables. $webproxy = New-Object -TypeName System.Net.WebProxy -ArgumentList $env:HTTP_PROXY, $true, $env:NO_PROXY } else { Write-Host "Creating WebProxy with 'HTTP_PROXY' environment variable. $webproxy = New-Object -TypeName System.Net.WebProxy -ArgumentList $env:HTTP_PROXY } $webClient.Proxy = $webproxy } Invoke-Expression -Command $webClient.DownloadString('http://bit.ly/GetPsGet') } try { # If the module isn't already loaded, ensure we can import it. if (-not (Get-Module -Name PsGet -ErrorAction SilentlyContinue)) { Import-Module -Name PsGet -Force -ErrorAction Stop } Install-Module -Name Pester -Force } catch { Write-Host "Installing from Github" $zipFile = Join-Path (Get-Item -Path $VerifierDownloadPath).FullName -ChildPath "pester.zip" if (-not (Test-Path $zipfile)) { $source = 'https://github.com/pester/Pester/archive/4.10.1.zip' $webClient = New-Object -TypeName Net.WebClient if ($env:HTTP_PROXY) { if ($env:NO_PROXY) { Write-Host "Creating WebProxy with 'HTTP_PROXY' and 'NO_PROXY' environment variables." $webproxy = New-Object -TypeName System.Net.WebProxy -ArgumentList $env:HTTP_PROXY, $true, $env:NO_PROXY } else { Write-Host "Creating WebProxy with 'HTTP_PROXY' environment variable." $webproxy = New-Object -TypeName System.Net.WebProxy -ArgumentList $env:HTTP_PROXY } $webClient.Proxy = $webproxy } [IO.File]::WriteAllBytes($zipfile, $webClient.DownloadData($source)) [GC]::Collect() Write-Host "Downloaded Pester.zip" } Write-Host "Creating Shell.Application COM object" $shellcom = New-Object -ComObject Shell.Application Write-Host "Creating COM object for zip file." $zipcomobject = $shellcom.Namespace($zipfile) Write-Host "Creating COM object for module destination." $destination = $shellcom.Namespace($VerifierModulePath) Write-Host "Unpacking zip file." $destination.CopyHere($zipcomobject.Items(), 0x610) Rename-Item -Path (Join-Path $VerifierModulePath -ChildPath "Pester-4.10.1") -NewName 'Pester' -Force } } } if (-not (Test-Module Pester)) { throw "Unable to install Pester. Please include Pester in your base image or install during your converge." } EOH end def restart_winrm_service 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 def download_test_files(state) 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 # 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. # # @return [Array] array of suite files # @api private def suite_test_folder @suite_test_folder ||= File.join(test_folder, config[:suite_name]) end def suite_level_glob Dir.glob(File.join(suite_test_folder, "*")) end def suite_verifier_level_glob Dir.glob(File.join(suite_test_folder, "*/**/*")) end def local_suite_files suite = suite_level_glob suite_verifier = suite_verifier_level_glob (suite << suite_verifier).flatten!.reject do |f| File.directory?(f) end end def sandboxify_path(path) File.join(sandbox_path, "suites", path.sub(%r{#{suite_test_folder}/}i, "")) end # Returns an Array of common helper filenames currently residing on the # local workstation. # # @return [Array] array of helper files # @api private def helper_files glob = Dir.glob(File.join(test_folder, "helpers", "*/**/*")) glob.reject { |f| File.directory?(f) } end # Copies all common testing helper files into the suites directory in # the sandbox. # # @api private 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 # Copies all test suite files into the suites directory in the sandbox. # # @api private def prepare_pester_tests info("Preparing to copy files from #{suite_test_folder} to the SUT.") local_suite_files.each do |src| dest = sandboxify_path(src) debug("Copying #{src} to #{dest}") FileUtils.mkdir_p(File.dirname(dest)) FileUtils.cp(src, dest, preserve: true) end end def prepare_powershell_module(name) FileUtils.mkdir_p(File.join(sandbox_path, "modules/#{name}")) FileUtils.cp( File.join(File.dirname(__FILE__), "../../support/powershell/#{name}/#{name}.psm1"), File.join(sandbox_path, "modules/#{name}/#{name}.psm1"), preserve: true ) end def prepare_powershell_modules info("Preparing to copy supporting powershell modules.") %w{PesterUtil}.each do |module_name| prepare_powershell_module module_name end end def test_folder return config[:test_base_path] if config[:test_folder].nil? absolute_test_folder end def absolute_test_folder path = (Pathname.new config[:test_folder]).realpath integration_path = File.join(path, "integration") return path unless Dir.exist?(integration_path) integration_path end end end end