diff --git a/CHANGELOG.md b/CHANGELOG.md index ae0a1f5..cf0f797 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ ## Unreleased +* Ports fixes for the following issues: + [Issue #505](https://github.com/PowerShell/xPSDesiredStateConfiguration/issues/505) + [Issue #590](https://github.com/PowerShell/xPSDesiredStateConfiguration/issues/590) + Changes to test helper Enter-DscResourceTestEnvironment so that it only + updates DSCResource.Tests when it is longer than 120 minutes since + it was last pulled. This is to improve performance of test execution + and reduce the likelihood of connectivity issues caused by inability to + pull DSCResource.Tests. * Fixes issue where MsiPackage Integration tests fail if the test HttpListener fails to start. Moves the test HttpListener objects to dynamically assigned, higher numbered ports to avoid conflicts with other services, and also checks diff --git a/Tests/TestHelpers/CommonTestHelper.psm1 b/Tests/TestHelpers/CommonTestHelper.psm1 index 74e5c58..ec41b2f 100644 --- a/Tests/TestHelpers/CommonTestHelper.psm1 +++ b/Tests/TestHelpers/CommonTestHelper.psm1 @@ -10,6 +10,18 @@ Set-StrictMode -Version 'Latest' #> $script:appVeyorAdministratorCredential = $null +<# + This is the name of the magic file that will be written to the .git folder + in DSCResource.Tests to determine the last time it was updated. +#> +$script:dscResourceTestsMagicFile = 'DSC_LAST_FETCH' + +<# + This is the number of minutes after which the DSCResource.Tests + will be updated. +#> +$script:dscResourceTestsRefreshAfterMinutes = 120 + <# String data for unit test names to be used with the generic test functions. Maps command names to the appropriate test names to insert when checking that @@ -65,24 +77,26 @@ Test-Path = test that the path {0} exists #> function Get-TestName { - [OutputType([String])] + [OutputType([System.String])] [CmdletBinding()] param ( [Parameter(Mandatory = $true)] - [String] + [System.String] $Command, - [Boolean] + [Parameter()] + [System.Boolean] $IsCalled = $true, - [String] + [Parameter()] + [System.String] $Custom = '' ) $testName = '' - if (-not [String]::IsNullOrEmpty($Custom)) + if (-not [System.String]::IsNullOrEmpty($Custom)) { $testName = ($testStrings.$Command -f $Custom) } @@ -115,7 +129,8 @@ function Invoke-ExpectedMocksAreCalledTest [CmdletBinding()] param ( - [Hashtable[]] + [Parameter()] + [System.Collections.Hashtable[]] $MocksCalled ) @@ -171,41 +186,46 @@ function Invoke-ExpectedMocksAreCalledTest The string that should be used to create the name of the test that checks for the correct error being thrown. #> -function Invoke-GenericUnitTest { +function Invoke-GenericUnitTest +{ [CmdletBinding()] param ( [Parameter(Mandatory = $true)] - [ScriptBlock] + [System.Management.Automation.ScriptBlock] $Function, [Parameter(Mandatory = $true)] - [Hashtable] + [System.Collections.Hashtable] $FunctionParameters, - [Hashtable[]] + [Parameter()] + [System.Collections.Hashtable[]] $MocksCalled, - [Boolean] + [Parameter()] + [System.Boolean] $ShouldThrow = $false, - [String] + [Parameter()] + [System.String] $ErrorMessage = '', - [String] + [Parameter()] + [System.String] $ErrorTestName = '' ) if ($ShouldThrow) { It "Should throw an error for $ErrorTestName" { - { $null = $($Function.Invoke($FunctionParameters)) } | Should Throw $ErrorMessage + { $null = $($Function.Invoke($FunctionParameters)) } | Should -Throw -ExpectedMessage $ErrorMessage } } else { It 'Should not throw' { - { $null = $($Function.Invoke($FunctionParameters)) } | Should Not Throw + { $null = $($Function.Invoke($FunctionParameters)) } | Should -Not -Throw } } @@ -239,19 +259,20 @@ function Invoke-GetTargetResourceUnitTest param ( [Parameter(Mandatory = $true)] - [Hashtable] + [System.Collections.Hashtable] $GetTargetResourceParameters, - [Hashtable[]] + [Parameter()] + [System.Collections.Hashtable[]] $MocksCalled, [Parameter(Mandatory = $true)] - [Hashtable] + [System.Collections.Hashtable] $ExpectedReturnValue ) It 'Should not throw' { - { $null = Get-TargetResource @GetTargetResourceParameters } | Should Not Throw + { $null = Get-TargetResource @GetTargetResourceParameters } | Should -Not -Throw } Invoke-ExpectedMocksAreCalledTest -MocksCalled $MocksCalled @@ -259,17 +280,17 @@ function Invoke-GetTargetResourceUnitTest $getTargetResourceResult = Get-TargetResource @GetTargetResourceParameters It 'Should return a Hashtable' { - $getTargetResourceResult -is [Hashtable] | Should Be $true + $getTargetResourceResult -is [System.Collections.Hashtable] | Should -Be $true } It "Should return a Hashtable with $($ExpectedReturnValue.Keys.Count) properties" { - $getTargetResourceResult.Keys.Count | Should Be $ExpectedReturnValue.Keys.Count + $getTargetResourceResult.Keys.Count | Should -Be $ExpectedReturnValue.Keys.Count } foreach ($key in $ExpectedReturnValue.Keys) { It "Should return a Hashtable with the $key property as $($ExpectedReturnValue.$key)" { - $getTargetResourceResult.$key | Should Be $ExpectedReturnValue.$key + $getTargetResourceResult.$key | Should -Be $ExpectedReturnValue.$key } } } @@ -302,37 +323,42 @@ function Invoke-GetTargetResourceUnitTest The string that should be used to create the name of the test that checks for the correct error being thrown. #> -function Invoke-SetTargetResourceUnitTest { +function Invoke-SetTargetResourceUnitTest +{ [CmdletBinding()] param ( [Parameter(Mandatory = $true)] - [Hashtable] + [System.Collections.Hashtable] $SetTargetResourceParameters, - [Hashtable[]] + [Parameter()] + [System.Collections.Hashtable[]] $MocksCalled, - [Boolean] + [Parameter()] + [System.Boolean] $ShouldThrow = $false, - [String] + [Parameter()] + [System.String] $ErrorMessage = '', - [String] + [Parameter()] + [System.String] $ErrorTestName = '' ) if ($ShouldThrow) { It "Should throw an error for $ErrorTestName" { - { $null = Set-TargetResource @SetTargetResourceParameters } | Should Throw $ErrorMessage + { $null = Set-TargetResource @SetTargetResourceParameters } | Should -Throw -ExpectedMessage $ErrorMessage } } else { It 'Should not throw' { - { $null = Set-TargetResource @SetTargetResourceParameters } | Should Not Throw + { $null = Set-TargetResource @SetTargetResourceParameters } | Should -Not -Throw } } @@ -366,19 +392,20 @@ function Invoke-TestTargetResourceUnitTest param ( [Parameter(Mandatory = $true)] - [Hashtable] + [System.Collections.Hashtable] $TestTargetResourceParameters, - [Hashtable[]] + [Parameter()] + [System.Collections.Hashtable[]] $MocksCalled, [Parameter(Mandatory = $true)] - [Boolean] + [System.Boolean] $ExpectedReturnValue ) It 'Should not throw' { - { $null = Test-TargetResource @TestTargetResourceParameters } | Should Not Throw + { $null = Test-TargetResource @TestTargetResourceParameters } | Should -Not -Throw } Invoke-ExpectedMocksAreCalledTest -MocksCalled $MocksCalled @@ -386,10 +413,12 @@ function Invoke-TestTargetResourceUnitTest $testTargetResourceResult = Test-TargetResource @TestTargetResourceParameters It "Should return $ExpectedReturnValue" { - $testTargetResourceResult | Should Be $ExpectedReturnValue + $testTargetResourceResult | Should -Be $ExpectedReturnValue } } + + <# .SYNOPSIS Tests that the Get-TargetResource method of a DSC Resource is not null, can be converted to a hashtable, and has the correct properties. @@ -408,16 +437,17 @@ function Test-GetTargetResourceResult ( [Parameter(Mandatory = $true)] [ValidateNotNull()] - [Hashtable] + [System.Collections.Hashtable] $GetTargetResourceResult, - [String[]] + [Parameter()] + [System.String[]] $GetTargetResourceResultProperties ) foreach ($property in $GetTargetResourceResultProperties) { - $GetTargetResourceResult[$property] | Should Not Be $null + $GetTargetResourceResult[$property] | Should -Not -Be $null } } @@ -430,13 +460,13 @@ function Test-GetTargetResourceResult #> function Test-IsLocalMachine { - [OutputType([Boolean])] + [OutputType([System.Boolean])] [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] - [String] + [System.String] $Scope ) @@ -467,7 +497,6 @@ function Test-IsLocalMachine NOTE: This is likely overkill; consider removing it. #> $networkAdapters = @(Get-CimInstance Win32_NetworkAdapterConfiguration) - foreach ($networkAdapter in $networkAdapters) { if ($null -ne $networkAdapter.IPAddress) @@ -500,23 +529,24 @@ function Test-IsLocalMachine #> function Wait-ScriptBlockReturnTrue { - [OutputType([Boolean])] + [OutputType([System.Boolean])] [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] - [ScriptBlock] + [System.Management.Automation.ScriptBlock] $ScriptBlock, - [Int] + [Parameter()] + [System.Int32] $TimeoutSeconds = 5 ) - $startTime = [DateTime]::Now + $startTime = [System.DateTime]::Now $invokeScriptBlockResult = $false - while (-not $invokeScriptBlockResult -and (([DateTime]::Now - $startTime).TotalSeconds -lt $TimeoutSeconds)) + while (-not $invokeScriptBlockResult -and (([System.DateTime]::Now - $startTime).TotalSeconds -lt $TimeoutSeconds)) { $invokeScriptBlockResult = $ScriptBlock.Invoke() Start-Sleep -Seconds 1 @@ -534,12 +564,12 @@ function Wait-ScriptBlockReturnTrue #> function Test-IsFileLocked { - [OutputType([Boolean])] + [OutputType([System.Boolean])] [CmdletBinding()] param ( [Parameter(Mandatory = $true)] - [String] + [System.String] $Path ) @@ -550,7 +580,7 @@ function Test-IsFileLocked try { - $content = Get-Content -Path $Path + Get-Content -Path $Path | Out-Null return $false } catch @@ -577,15 +607,16 @@ function Test-IsFileLocked #> function Test-SetTargetResourceWithWhatIf { - [OutputType([Boolean])] + [OutputType([System.Boolean])] [CmdletBinding()] param ( [Parameter(Mandatory = $true)] - [Hashtable] + [System.Collections.Hashtable] $Parameters, - [String[]] + [Parameter()] + [System.String[]] $ExpectedOutput ) @@ -609,7 +640,7 @@ function Test-SetTargetResourceWithWhatIf Wait-ScriptBlockReturnTrue -ScriptBlock {-not (Test-IsFileLocked -Path $transcriptPath)} $transcriptContent = Get-Content -Path $transcriptPath -Raw - $transcriptContent | Should Not Be $null + $transcriptContent | Should -Not -Be $null $regexString = '\*+[^\*]*\*+' @@ -627,13 +658,13 @@ function Test-SetTargetResourceWithWhatIf if ($null -eq $ExpectedOutput -or $ExpectedOutput.Count -eq 0) { - [String]::IsNullOrEmpty($transcriptContent) | Should Be $true + [System.String]::IsNullOrEmpty($transcriptContent) | Should -Be $true } else { foreach ($expectedOutputPiece in $ExpectedOutput) { - $transcriptContent.Contains($expectedOutputPiece) | Should Be $true + $transcriptContent.Contains($expectedOutputPiece) | Should -Be $true } } } @@ -707,51 +738,33 @@ function Get-AppVeyorAdministratorCredential #> function Enter-DscResourceTestEnvironment { - [OutputType([Hashtable])] + [OutputType([System.Collections.Hashtable])] [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] - [String] + [System.String] $DscResourceModuleName, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] - [String] + [System.String] $DscResourceName, [Parameter(Mandatory = $true)] [ValidateSet('Unit', 'Integration')] - [String] + [System.String] $TestType ) - $testsFolderPath = Split-Path -Path $PSScriptRoot -Parent - $moduleRootPath = Split-Path -Path $testsFolderPath -Parent + $moduleRootPath = Split-Path -Path $PSScriptRoot -Parent $dscResourceTestsPath = Join-Path -Path $moduleRootPath -ChildPath 'DSCResource.Tests' $testHelperFilePath = Join-Path -Path $dscResourceTestsPath -ChildPath 'TestHelper.psm1' - if (-not (Test-Path -Path $dscResourceTestsPath)) + if (Test-DscResourceTestsNeedsInstallOrUpdate) { - Push-Location $moduleRootPath - git clone 'https://github.com/PowerShell/DscResource.Tests' --quiet - Pop-Location - } - else - { - $gitInstalled = $null -ne (Get-Command -Name 'git' -ErrorAction 'SilentlyContinue') - - if ($gitInstalled) - { - Push-Location $dscResourceTestsPath - git pull origin dev --quiet - Pop-Location - } - else - { - Write-Verbose -Message 'Git not installed. Leaving current DSCResource.Tests as is.' - } + Install-DscResourceTestsModule } Import-Module -Name $testHelperFilePath @@ -762,6 +775,155 @@ function Enter-DscResourceTestEnvironment -TestType $TestType } +<# + .SYNOPSIS + Tests to see if DSCResource.Tests needs to be downloaded + or updated. + + .DESCRIPTION + This function returns true if the DSCResource.Tests + content needs to be downloaded or updated. + + A magic file in the .Git folder of DSCResource.Tests + is used to determine if the repository needs to be + updated. + + If the last write time of the magic file is over a + specified number of minutes old then this will cause + the function to return true. + + .PARAMETER RefreshAfterMinutes + The number of minutes old the magic file should be + before requiring an update. Defaults to the value + defined in $script:dscResourceTestsRefreshAfterMinutes +#> +function Test-DscResourceTestsNeedsInstallOrUpdate +{ + [OutputType([System.Boolean])] + [CmdletBinding()] + param + ( + [Parameter()] + [System.Int32] + $RefreshAfterMinutes = $script:dscResourceTestsRefreshAfterMinutes + ) + + $moduleRootPath = Split-Path -Path $PSScriptRoot -Parent + $dscResourceTestsPath = Join-Path -Path $moduleRootPath -ChildPath 'DSCResource.Tests' + + if (Test-Path -Path $dscResourceTestsPath) + { + $magicFilePath = Get-DscResourceTestsMagicFilePath -DscResourceTestsPath $DscResourceTestsPath + + if (Test-Path -Path $magicFilePath) + { + $magicFileLastWriteTime = (Get-Item -Path $magicFilePath).LastWriteTime + $magicFileAge = New-TimeSpan -End (Get-Date) -Start $magicFileLastWriteTime + + if ($magicFileAge.Minutes -lt $RefreshAfterMinutes) + { + Write-Debug -Message ('DSCResource.Tests was last updated {0} minutes ago. Update not required.' -f $magicFileAge.Minutes) + return $false + } + else + { + Write-Verbose -Message ('DSCResource.Tests was last updated {0} minutes ago. Update required.' -f $magicFileAge.Minutes) -Verbose + } + } + } + + return $true +} + +<# + .SYNOPSIS + Installs the DSCResource.Tests content. + + .DESCRIPTION + This function uses Git to install or update the + DSCResource.Tests repository. + + It will then create or update the magic file in + the .git folder in the DSCResource.Tests folder. + + If Git is not installed and the DSCResource.Tests + folder is not available then an exception will + be thrown. + + If the DSCResource.Tests folder does exist but + Git is not installed then a warning will be + displayed and the repository will not be pulled. +#> +function Install-DscResourceTestsModule +{ + [CmdletBinding()] + param + ( + ) + + $moduleRootPath = Split-Path -Path $PSScriptRoot -Parent + $dscResourceTestsPath = Join-Path -Path $moduleRootPath -ChildPath 'DSCResource.Tests' + $gitInstalled = $null -ne (Get-Command -Name 'git' -ErrorAction 'SilentlyContinue') + $writeMagicFile = $false + + if (Test-Path -Path $dscResourceTestsPath) + { + if ($gitInstalled) + { + Push-Location -Path $dscResourceTestsPath + Write-Verbose -Message 'Updating DSCResource.Tests.' -Verbose + & git @('pull','origin','dev','--quiet') + $writeMagicFile = $true + Pop-Location + } + else + { + Write-Warning -Message 'Git not installed. DSCResource.Tests will not be updated.' + } + } + else + { + if (-not $gitInstalled) + { + throw 'Git not installed. Can not pull DSCResource.Tests.' + } + + Push-Location -Path $moduleRootPath + Write-Verbose -Message 'Cloning DSCResource.Tests.' -Verbose + & git @('clone','https://github.com/PowerShell/DscResource.Tests','--quiet') + $writeMagicFile = $true + Pop-Location + } + + if ($writeMagicFile) + { + # Write the magic file + $magicFilePath = Get-DscResourceTestsMagicFilePath -DscResourceTestsPath $DscResourceTestsPath + $null = Set-Content -Path $magicFilePath -Value (Get-Date) -Force + } +} + +<# + .SYNOPSIS + Gets the full path of the magic file used to + determine the last date/time the DSCResource.Tests + folder was updated. + + .PARAMETER DscResourceTestsPath + The path to the folder that contains DSCResource.Tests. +#> +function Get-DscResourceTestsMagicFilePath +{ + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $DscResourceTestsPath + ) + + return $DscResourceTestsPath | Join-Path -ChildPath '.git' | Join-Path -ChildPath $script:dscResourceTestsMagicFile +} + <# .SYNOPSIS Exits the specified DSC Resource test environment. @@ -776,12 +938,11 @@ function Exit-DscResourceTestEnvironment ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] - [Hashtable] + [System.Collections.Hashtable] $TestEnvironment ) - $testsFolderPath = Split-Path -Path $PSScriptRoot -Parent - $moduleRootPath = Split-Path -Path $testsFolderPath -Parent + $moduleRootPath = Split-Path -Path $PSScriptRoot -Parent $dscResourceTestsPath = Join-Path -Path $moduleRootPath -ChildPath 'DSCResource.Tests' $testHelperFilePath = Join-Path -Path $dscResourceTestsPath -ChildPath 'TestHelper.psm1'