DscResource.Test.psm1
#Region './Private/ConvertTo-OrderedDictionary.ps1' 0 function ConvertTo-OrderedDictionary { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseOutputTypeCorrectly', '')] [CmdletBinding()] [outputType([System.Object])] param ( [Parameter(ValueFromPipeline = $true)] [Object] $InputObject ) if ($null -eq $InputObject) { return $null } if ($InputObject -is [System.Collections.IDictionary]) { $hashKeys = $InputObject.Keys # Making the Ordered Dict Case Insensitive $result = [ordered]@{ } foreach ($Key in $hashKeys) { $result[$Key] = ConvertTo-OrderedDictionary -InputObject $InputObject[$Key] } $result } elseif ($InputObject -is [System.Collections.IEnumerable] -and $InputObject -isNot [string]) { $collection = @( foreach ($object in $InputObject) { ConvertTo-OrderedDictionary -InputObject $object } ) , $collection } elseif ($InputObject -is [PSCustomObject]) { $result = [ordered]@{ } foreach ($property in $InputObject.PSObject.Properties) { $result[$property.Name] = ConvertTo-OrderedDictionary -InputObject $property.Value } $result } else { $InputObject } } #EndRegion './Private/ConvertTo-OrderedDictionary.ps1' 54 #Region './Private/Get-ClassResourceNameFromFile.ps1' 0 <# .SYNOPSIS Retrieves the name(s) of any DSC class resources from a PowerShell file. .PARAMETER FilePath The full path to the file to test. .EXAMPLE Get-ClassResourceNameFromFile -FilePath 'c:\mymodule\myclassmodule.psm1' This command will get any DSC class resource names from the myclassmodule module. #> function Get-ClassResourceNameFromFile { [OutputType([String[]])] [CmdletBinding()] param ( [Parameter(ValueFromPipeline = $true, Mandatory = $true)] [String] $FilePath ) $classResourceNames = [String[]]@() if (Test-FileContainsClassResource -FilePath $FilePath) { $fileAst = [System.Management.Automation.Language.Parser]::ParseFile($FilePath, [ref]$null, [ref]$null) $typeDefinitionAsts = $fileAst.FindAll( {$args[0] -is [System.Management.Automation.Language.TypeDefinitionAst]}, $false) foreach ($typeDefinitionAst in $typeDefinitionAsts) { if ($typeDefinitionAst.Attributes.TypeName.Name -ieq 'DscResource') { $classResourceNames += $typeDefinitionAst.Name } } } return $classResourceNames } #EndRegion './Private/Get-ClassResourceNameFromFile.ps1' 41 #Region './Private/Get-CurrentModuleBase.ps1' 0 function Get-CurrentModuleBase { [CmdletBinding()] [OutputType([System.String])] param ( ) return $MyInvocation.MyCommand.Module.ModuleBase } #EndRegion './Private/Get-CurrentModuleBase.ps1' 9 #Region './Private/Get-DscResourceTestConfiguration.ps1' 0 function Get-DscResourceTestConfiguration { [cmdletBinding()] param ( [Parameter()] [Alias('Path')] [Object] $Configuration = (Join-Path $PWD '.MetaTestOptIn.json') ) if ($Configuration -is [System.Collections.IDictionary]) { Write-Debug "Configuration Object is a Dictionary" } elseif ($Configuration -is [System.Management.Automation.PSCustomObject]) { Write-Debug "Configuration Object is a PSCustomObject" } elseif ( $Configuration -is [System.String]) { Write-Debug "Configuration Object is a String, probably a Path" $Configuration = Get-StructuredObjectFromFile -Path $Configuration } else { throw "Could not resolve Configuration parameter $Configuration of Type $($Configuration.GetType().ToString())" } $NormalizedConfigurationObject = ConvertTo-OrderedDictionary -InputObject $Configuration return $NormalizedConfigurationObject } #EndRegion './Private/Get-DscResourceTestConfiguration.ps1' 33 #Region './Private/Get-FileParseError.ps1' 0 <# .SYNOPSIS Retrieves the parse errors for the given file. .PARAMETER FilePath The path to the file to get parse errors for. #> function Get-FileParseError { [OutputType([System.Management.Automation.Language.ParseError[]])] [CmdletBinding()] param ( [Parameter(ValueFromPipeline = $true, Mandatory = $true)] [String] $FilePath ) $parseErrors = $null $null = [System.Management.Automation.Language.Parser]::ParseFile($FilePath, [ref] $null, [ref] $parseErrors) return $parseErrors } #EndRegion './Private/Get-FileParseError.ps1' 23 #Region './Private/Get-ModuleScriptResourceName.ps1' 0 <# .SYNOPSIS Retrieves the names of all script resources for the given module. .PARAMETER ModulePath The path to the module to retrieve the script resource names of. #> function Get-ModuleScriptResourceName { [OutputType([String[]])] [CmdletBinding()] param ( [Parameter(ValueFromPipeline = $true, Mandatory = $true)] [String] $ModulePath ) $scriptResourceNames = @() $dscResourcesFolderFilePath = Join-Path -Path $ModulePath -ChildPath 'DscResources' $mofSchemaFiles = Get-ChildItem -Path $dscResourcesFolderFilePath -Filter '*.schema.mof' -File -Recurse foreach ($mofSchemaFile in $mofSchemaFiles) { $scriptResourceName = $mofSchemaFile.BaseName -replace '.schema', '' $scriptResourceNames += $scriptResourceName } return $scriptResourceNames } #EndRegion './Private/Get-ModuleScriptResourceName.ps1' 32 #Region './Private/Get-Psm1FileList.ps1' 0 <# .SYNOPSIS Retrieves all .psm1 files under the given file path. .PARAMETER FilePath The root file path to gather the .psm1 files from. #> function Get-Psm1FileList { [OutputType([Object[]])] [CmdletBinding()] param ( [Parameter(ValueFromPipeline = $true, Mandatory = $true)] [String] $FilePath ) return Get-ChildItem -Path $FilePath -Filter '*.psm1' -File -Recurse } #EndRegion './Private/Get-Psm1FileList.ps1' 20 #Region './Private/Get-RelativePathFromModuleRoot.ps1' 0 <# .SYNOPSIS This returns a string containing the relative path from the module root. .PARAMETER FilePath The file path to remove the module root path from. .PARAMETER ModuleRootFilePath The root path to remove from the file path. #> function Get-RelativePathFromModuleRoot { param ( [Parameter(Mandatory = $true)] [System.String] $FilePath, [Parameter(Mandatory = $true)] [System.String] $ModuleRootFilePath ) <# Removing the module root path from the file path so that the path doesn't get so long in the Pester output. #> return ($FilePath -replace [Regex]::Escape($ModuleRootFilePath), '').Trim([io.path]::DirectorySeparatorChar) } #EndRegion './Private/Get-RelativePathFromModuleRoot.ps1' 30 #Region './Private/Get-StructuredObjectFromFile.ps1' 0 function Get-StructuredObjectFromFile { [cmdletBinding()] param ( [Parameter()] [String] $Path ) $ioPath = [System.IO.FileInfo]($PSCmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path)) switch -regex ($ioPath.Extension) { '^\.psd1$' { $ObjectFromFile = Import-PowerShellDataFile -Path $ioPath -ErrorAction Stop } '^\.y[a]?ml$' { Import-Module Powershell-yaml -ErrorAction Stop $FileContent = Get-Content -Raw -Path $ioPath -ErrorAction Stop $ObjectFromFile = ConvertFrom-Yaml -Ordered -Yaml $FileContent -ErrorAction Stop } '^\.json$' { $FileContent = Get-Content -Raw -Path $ioPath -ErrorAction Stop $ObjectFromFile = ConvertFrom-Json -InputObject $FileContent -ErrorAction Stop } Default { throw "File extension $($ioPath.Extension) not recognized." } } return $ObjectFromFile } #EndRegion './Private/Get-StructuredObjectFromFile.ps1' 39 #Region './Private/Get-SuppressedPSSARuleNameList.ps1' 0 <# .SYNOPSIS Retrieves the list of suppressed PSSA rules in the file at the given path. .PARAMETER FilePath The path to the file to retrieve the suppressed rules of. #> function Get-SuppressedPSSARuleNameList { [OutputType([String[]])] [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [String] $FilePath ) $suppressedPSSARuleNames = [String[]]@() $fileAst = [System.Management.Automation.Language.Parser]::ParseFile($FilePath, [ref]$null, [ref]$null) # Overall file attributes $attributeAsts = $fileAst.FindAll( {$args[0] -is [System.Management.Automation.Language.AttributeAst]}, $true) foreach ($attributeAst in $attributeAsts) { if ([System.Diagnostics.CodeAnalysis.SuppressMessageAttribute].FullName.ToLower().Contains($attributeAst.TypeName.FullName.ToLower())) { $suppressedPSSARuleNames += $attributeAst.PositionalArguments.Extent.Text } } return $suppressedPSSARuleNames } #EndRegion './Private/Get-SuppressedPSSARuleNameList.ps1' 37 #Region './Private/Get-TextFilesList.ps1' 0 <# .SYNOPSIS Retrieves all text files under the given root file path. .PARAMETER Root The root file path under which to retrieve all text files. .NOTES Retrieves all files with the '.gitignore', '.gitattributes', '.ps1', '.psm1', '.psd1', '.json', '.xml', '.cmd', or '.mof' file extensions. #> function Get-TextFilesList { [OutputType([System.IO.FileInfo[]])] [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [String] $Root ) $textFileExtensions = @('.gitignore', '.gitattributes', '.ps1', '.psm1', '.psd1', '.json', '.xml', '.cmd', '.mof', '.md', '.js', '.yml') return Get-ChildItem -Path $Root -File -Recurse | Where-Object { $textFileExtensions -contains $_.Extension } } #EndRegion './Private/Get-TextFilesList.ps1' 27 #Region './Private/Test-FileContainsClassResource.ps1' 0 <# .SYNOPSIS Tests if a PowerShell file contains a DSC class resource. .PARAMETER FilePath The full path to the file to test. .EXAMPLE Test-ContainsClassResource -ModulePath 'c:\mymodule\myclassmodule.psm1' This command will test myclassmodule for the presence of any class-based DSC resources. #> function Test-FileContainsClassResource { [OutputType([Boolean])] [CmdletBinding()] param ( [Parameter(ValueFromPipeline = $true, Mandatory = $true)] [String] $FilePath ) $fileAst = [System.Management.Automation.Language.Parser]::ParseFile($FilePath, [ref]$null, [ref]$null) foreach ($fileAttributeAst in $fileAst.FindAll( {$args[0] -is [System.Management.Automation.Language.AttributeAst]}, $false)) { if ($fileAttributeAst.Extent.Text -ieq '[DscResource()]') { return $true } } return $false } #EndRegion './Private/Test-FileContainsClassResource.ps1' 36 #Region './Private/Test-FileHasByteOrderMark.ps1' 0 <# .SYNOPSIS Tests if a file contains Byte Order Mark (BOM). .PARAMETER FilePath The file path to evaluate. #> function Test-FileHasByteOrderMark { param ( [Parameter(Mandatory = $true)] [System.String] $FilePath ) $getContentParameters = @{ Path = $FilePath ReadCount = 3 TotalCount = 3 } # Need to treat Windows Powershell and PowerShell Core different. if ($PSVersionTable.PSEdition -eq 'Core') { $getContentParameters['AsByteStream'] = $true } else { $getContentParameters['Encoding'] = 'Byte' } # This reads the first three bytes of the first row. $firstThreeBytes = Get-Content @getContentParameters # Check for the correct byte order (239,187,191) which equal the Byte Order Mark (BOM). return ($firstThreeBytes[0] -eq 239 ` -and $firstThreeBytes[1] -eq 187 ` -and $firstThreeBytes[2] -eq 191) } #EndRegion './Private/Test-FileHasByteOrderMark.ps1' 41 #Region './Private/Test-FileInUnicode.ps1' 0 <# .SYNOPSIS Tests if a file is encoded in Unicode. .PARAMETER FileInfo The file to test. #> function Test-FileInUnicode { [OutputType([Boolean])] [CmdletBinding()] param ( [Parameter(ValueFromPipeline = $true, Mandatory = $true)] [System.IO.FileInfo] $FileInfo ) $filePath = $FileInfo.FullName $fileBytes = [System.IO.File]::ReadAllBytes($filePath) $zeroBytes = @( $fileBytes -eq 0 ) return ($zeroBytes.Length -ne 0) } #EndRegion './Private/Test-FileInUnicode.ps1' 24 #Region './Private/Test-ModuleContainsClassResource.ps1' 0 <# .SYNOPSIS Tests if a module contains a class resource. .PARAMETER ModulePath The path to the module to test. #> function Test-ModuleContainsClassResource { [OutputType([Boolean])] [CmdletBinding()] param ( [Parameter(ValueFromPipeline = $true, Mandatory = $true)] [String] $ModulePath ) $psm1Files = Get-Psm1FileList -FilePath $ModulePath foreach ($psm1File in $psm1Files) { if (Test-FileContainsClassResource -FilePath $psm1File.FullName) { return $true } } return $false } #EndRegion './Private/Test-ModuleContainsClassResource.ps1' 31 #Region './Private/Test-TestShouldBeSkipped.ps1' 0 function Test-TestShouldBeSkipped { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [System.String[]] $TestNames, [Parameter(Mandatory = $true)] [AllowNull()] [System.String[]] $Tag, [Parameter(Mandatory = $true)] [AllowNull()] [System.String[]] $ExcludeTag ) if ($ExcludeTag) { $IsTagExcluded = Compare-Object -ReferenceObject $TestNames -DifferenceObject $ExcludeTag -IncludeEqual -ExcludeDifferent } else { $IsTagExcluded = $false } $IsTagIncluded = Compare-Object -ReferenceObject $TestNames -DifferenceObject $Tag -IncludeEqual -ExcludeDifferent # Should be skipped if It's excluded or Tags are in use and it's not included $ShouldBeSkipped = ($IsTagExcluded -or ($Tag -and -Not $isTagIncluded)) if ($ShouldBeSkipped) { Write-Warning "The tests for $($TestNames -join ',') is not being enforced. Please Opt-in!" } return $ShouldBeSkipped } #EndRegion './Private/Test-TestShouldBeSkipped.ps1' 40 #Region './Public/Invoke-DscResourceTest.ps1' 0 function Invoke-DscResourceTest { [CmdletBinding(DefaultParameterSetName = 'ByProjectPath')] param ( [Parameter(ParameterSetName = 'ByModuleNameOrPath', Position = 0)] [System.String] ${Module}, [Parameter(ParameterSetName = 'ByModuleSpecification', Position = 0)] [Microsoft.PowerShell.Commands.ModuleSpecification] $FullyQualifiedModule, [Parameter(ParameterSetName = 'ByProjectPath', Position = 0)] [System.String] ${ProjectPath}, [Parameter(Position = 1)] [Alias('Path', 'relative_path')] [System.Object[]] ${Script}, [Parameter(Position = 2)] [Alias('Name')] [string[]] ${TestName}, [Parameter(Position = 3)] [switch] ${EnableExit}, [Parameter(Position = 5)] [Alias('Tags')] [string[]] ${Tag}, [Parameter()] [string[]] ${ExcludeTag}, [Parameter()] [switch] ${PassThru}, [Parameter()] [System.Object[]] ${CodeCoverage}, [Parameter()] [string] ${CodeCoverageOutputFile}, [Parameter()] [ValidateSet('JaCoCo')] [string] ${CodeCoverageOutputFileFormat}, [Parameter()] [switch] ${Strict}, [Parameter(ParameterSetName = 'NewOutputSet', Mandatory = $true)] [string] ${OutputFile}, [Parameter(ParameterSetName = 'NewOutputSet')] [ValidateSet('NUnitXml', 'JUnitXml')] [string] ${OutputFormat}, [Parameter()] [switch] ${Quiet}, [Parameter()] [System.Object] ${PesterOption}, [Parameter()] [Pester.OutputTypes] ${Show}, [Parameter()] [Hashtable] $Settings ) begin { # Make sure Invoke-DscResourceTest runs against the Built Module either: # By $Module (Name, Path, ModuleSpecification): enables to run some tests on installed modules (even without source) # By $ProjectPath (detect source from there based on .psd1): Target both the source when relevant and the expected files switch ($PSCmdlet.ParameterSetName) { 'ByModuleNameOrPath' { Write-Verbose "Calling DscResource Test by Module Name (Or Path)" if (!$PSBoundParameters.ContainsKey('Script')) { $PSBoundParameters['Script'] = Join-Path -Path $MyInvocation.MyCommand.Module.ModuleBase -ChildPath 'Tests\QA\BuiltModule' } $null = $PSBoundParameters.Remove('Module') $ModuleUnderTest = Import-Module -Name $Module -ErrorAction Stop -Force -PassThru } 'ByModuleSpecification' { Write-Verbose "Calling DscResource Test by Module Specification" if (!$PSBoundParameters.ContainsKey('Script')) { $PSBoundParameters['Script'] = Join-Path -Path $MyInvocation.MyCommand.Module.ModuleBase -ChildPath 'Tests\QA\BuiltModule' } $null = $PSBoundParameters.Remove('FullyQualifiedModule') $ModuleUnderTest = Import-Module -FullyQualifiedName $FullyQualifiedModule -Force -PassThru -ErrorAction Stop } 'ByProjectPath' { Write-Verbose "Calling DscResource Test by Project Path" if (!$ProjectPath) { $ProjectPath = $PWD.Path } try { $null = $PSBoundParameters.Remove('ProjectPath') } catch { Write-Debug -Message "The function was called via default param set. Using `$PWD for Project Path" } if (!$PSBoundParameters.ContainsKey('Script')) { $PSBoundParameters['Script'] = Join-Path -Path $MyInvocation.MyCommand.Module.ModuleBase -ChildPath 'Tests\QA' } # Find the Source Manifest under ProjectPath $SourceManifest = ((Get-ChildItem -Path $ProjectPath\*\*.psd1).Where{ ($_.Directory.Name -match 'source|src' -or $_.Directory.Name -eq $_.BaseName) -and $(try { Test-ModuleManifest $_.FullName -ErrorAction Stop } catch { $false } ) }) $SourcePath = $SourceManifest.Directory.FullName $GetOutputModuleParams = @{ Path = (Join-Path $ProjectPath 'output') Include = $SourceManifest.Name Recurse = $true Exclude = 'RequiredModules' ErrorAction = 'Stop' } Write-Verbose ( "Finding Output Module with `r`n {0}" -f ($GetOutputModuleParams | Format-Table -AutoSize | Out-String) ) $ModulePsd1 = Get-ChildItem @GetOutputModuleParams $ModuleUnderTest = Import-Module -Name $ModulePsd1 -ErrorAction Stop -PassThru } } # In case of ByProjectPath Opt-ins will be done by tags: # The Describe Name will be one of the Tag for the Describe block # If a Opt-In file is found, it will default to auto-populate -Tag (cumulative from Command parameters) if ($ProjectPath) { $ExpectedMetaOptInFile = Join-Path -Path $ProjectPath -ChildPath '.MetaTestOptIn.json' if ($PSCmdlet.ParameterSetName -eq 'ByProjectPath' -and (Test-Path $ExpectedMetaOptInFile)) { Write-Verbose -Message "Loading OptIns from $ExpectedMetaOptInFile" $OptIns = Get-StructuredObjectFromFile -Path $ExpectedMetaOptInFile -ErrorAction Stop } # Opt-Outs should be preferred, and we can do similar ways with ExcludeTags $ExpectedMetaOptOutFile = Join-Path -Path $ProjectPath -ChildPath '.MetaTestOptOut.json' if ($PSCmdlet.ParameterSetName -eq 'ByProjectPath' -and (Test-Path $ExpectedMetaOptOutFile)) { Write-Verbose -Message "Loading OptOuts from $ExpectedMetaOptOutFile" $OptOuts = Get-StructuredObjectFromFile -Path $ExpectedMetaOptOutFile -ErrorAction Stop } } # For each Possible parameters, use BoundParameters if exists, or use $Settings.ParameterName if exists otherwise $PossibleParamName = $PSCmdlet.MyInvocation.MyCommand.Parameters.Name foreach ($ParamName in $PossibleParamName) { if ( !$PSBoundParameters.ContainsKey($ParamName) -and ($ParamValue = $Settings.($ParamName)) ) { Write-Verbose -Message "Adding setting $ParamName" $PSBoundParameters.Add($ParamName, $ParamValue) } } $newTag = @() $newExcludeTag = @() # foreach OptIns, add them to `-Tag`, unless in the ExcludeTags or already in Tag foreach ($OptInTag in $OptIns) { if ( $OptInTag -notIn $PSBoundParameters['ExcludeTag'] -and $OptInTag -notIn $PSBoundParameters['Tag'] ) { Write-Debug -Message "Adding tag $OptInTag" $newTag += $OptInTag } } if ($newTag.Count -gt 0) { $PSBoundParameters['Tag'] = $newTag } # foreach OptOuts, add them to `-ExcludeTag`, unless in `-Tag` foreach ($OptOutTag in $OptOuts) { if ( $OptOutTag -notIn $PSBoundParameters['Tag'] -and $OptOutTag -notIn $PSBoundParameters['ExcludeTag'] ) { Write-Debug -Message "Adding ExcludeTag $OptOutTag" $newExcludeTag += $OptOutTag } } if ($newExcludeTag.Count -gt 0) { $PSBoundParameters['ExcludeTag'] = $newExcludeTag } # This won't display the warning message for the skipped blocks # But should save time by not running initialization code within a Describe Block # And we can add such warning if we create a static list of the things we can opt-in # I'd prefer to not keep anything static, and AST risks not to cover 100% (maybe...), and OptOut is prefered # Most tests should run against the built module # PSSA could be run against source, or against built module & convert lines/file $ModuleUnderTestManifest = Join-Path -Path $ModuleUnderTest.ModuleBase -ChildPath "$($ModuleUnderTest.Name).psd1" $ScriptItems = foreach ($item in $PSBoundParameters['Script']) { if ($item -is [System.Collections.IDictionary]) { if ($item['Parameters'] -isNot [System.Collections.IDictionary]) { $item['Parameters'] = @{ } } $item['Parameters']['ModuleBase'] = $ModuleUnderTest.ModuleBase $item['Parameters']['ModuleName'] = $ModuleUnderTest.Name $item['Parameters']['ModuleManifest'] = $ModuleUnderTestManifest $item['Parameters']['ProjectPath'] = $ProjectPath $item['Parameters']['SourcePath'] = $SourcePath $item['Parameters']['SourceManifest'] = $SourceManifest.FullName $item['Parameters']['Tag'] = $PSBoundParameters['Tag'] $item['Parameters']['ExcludeTag'] = $PSBoundParameters['ExcludeTag'] } else { $item = @{ Path = $item Parameters = @{ ModuleBase = $ModuleUnderTest.ModuleBase ModuleName = $ModuleUnderTest.Name ModuleManifest = $ModuleUnderTestManifest ProjectPath = $ProjectPath SourcePath = $SourcePath SourceManifest = $SourceManifest.FullName Tag = $PSBoundParameters['Tag'] ExcludeTag = $PSBoundParameters['ExcludeTag'] } } } $item } $PSBoundParameters['Script'] = $ScriptItems # Below is default command proxy handling try { $outBuffer = $null if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer)) { $PSBoundParameters['OutBuffer'] = 1 } $wrappedCmd = Get-Command -CommandType Function -Name Invoke-Pester $scriptCmd = { & $wrappedCmd @PSBoundParameters } $steppablePipeline = $scriptCmd.GetSteppablePipeline() $steppablePipeline.Begin($PSCmdlet) } catch { throw } } process { try { $steppablePipeline.Process($_) } catch { throw } } end { try { $steppablePipeline.End() } catch { throw } } <# .ForwardHelpTargetName Invoke-Pester .ForwardHelpCategory Function #> } #EndRegion './Public/Invoke-DscResourceTest.ps1' 341 |