PSQualityCheck.psm1
#Region '.\private\ConvertHelp.ps1' 0 function ConvertHelp { <# .SYNOPSIS Convert the help comment into an object .DESCRIPTION Convert the help comment into an object containing all the elements from the help comment .PARAMETER Help A string containing the Help Comment .EXAMPLE $helpObject = ConvertHelp -Help $Help #> [CmdletBinding()] [OutputType([HashTable], [System.Exception])] param ( [parameter(Mandatory = $true)] [string]$Help ) # These are the possible Help Comment elements that the script will look for # .SYNOPSIS # .DESCRIPTION # .PARAMETER # .EXAMPLE # .INPUTS # .OUTPUTS # .NOTES # .LINK # .COMPONENT # .ROLE # .FUNCTIONALITY # .FORWARDHELPTARGETNAME # .FORWARDHELPCATEGORY # .REMOTEHELPRUNSPACE # .EXTERNALHELP # This function will go through the help and work out which elements are where and what text they contain try { if (-not( $Help.StartsWith("<#") -and $Help.EndsWith("#>") )) { throw "Help does not appear to be a comment block" } $helpElementsToFind = '.SYNOPSIS', '.DESCRIPTION', '.PARAMETER', '.EXAMPLE', '.INPUTS', '.OUTPUTS', '.NOTES', '.LINK', '.COMPONENT', '.ROLE', '.FUNCTIONALITY', '.FORWARDHELPTARGETNAME', '.FORWARDHELPCATEGORY', '.REMOTEHELPRUNSPACE', '.EXTERNALHELP' $commentArray = ($Help -split '\n').Trim() $foundElements = @{} $numFound = 0 $lastHelpElement = $null for ($line = 0; $line -lt $commentArray.Count; $line++) { # get the first 'word' of the help comment. This is required so that we can # match '.PARAMETER' since it has a parameter name after it $helpElementKey = ($commentArray[$line] -split " ")[0] # Get the value of the Help Comment (the parameter name) try { $helpElementName = ($commentArray[$line] -split " ")[1] } catch { $helpElementName = "" } if ($helpElementsToFind -contains $helpElementKey) { $numFound++ if ($numFound -ge 2) { # if it's the second element then we must set the help comment text of the # previous element to the found text so far, then reset it $lastElement = @($foundElements[$lastHelpElement]) $lastElement[$lastElement.Count - 1].Text = $helpData $foundElements[$lastHelpElement] = $lastElement $helpData = $null } # this should be an array of HashTables # each hash table will contain the properties LineNumber, Name & Text $currentElement = @($foundElements[$helpElementKey]) $newElement = @{} $newElement.LineNumber = $line $newElement.Name = $helpElementName $newElement.Text = "" if ($null -eq $currentElement[0]) { $currentElement = $newElement } else { $currentElement += $newElement } $foundElements[$helpElementKey] = $currentElement $lastHelpElement = $helpElementKey } else { if ($numFound -ge 1 -and $line -lt ($commentArray.Count - 1)) { $helpData += $commentArray[$line] } } } if ( -not ([string]::IsNullOrEmpty($lastHelpElement))) { $currentElement = @($foundElements[$lastHelpElement]) $currentElement[$currentElement.Count - 1].Text = $helpData $foundElements[$lastHelpElement] = $currentElement } return $foundElements } catch { throw $_.Exception.Message } } #EndRegion '.\private\ConvertHelp.ps1' 150 #Region '.\private\ExportFunctionsFromModule.ps1' 0 function ExportFunctionsFromModule { <# .SYNOPSIS Export functions from a PowerShell module (.psm1) .DESCRIPTION Takes a PowerShell module and outputs a single file for each function containing the code for that function .PARAMETER Path A string Path containing the full file name and path to the module .PARAMETER ExtractPath A string Path containing the full path to the extraction folder .EXAMPLE ExportFunctionsFromModule -Path 'c:\path.to\module.psm1' -ExtractPath 'c:\extract' #> [CmdletBinding()] [OutputType([System.Void])] param ( [parameter(Mandatory = $true)] [string]$Path, [parameter(Mandatory = $true)] [string]$ExtractPath ) try { $fileProperties = (Get-Item -LiteralPath $Path) if ($fileProperties.Extension -ne ".psm1") { throw "Passed file does not appear to be a PowerShell module" } $moduleName = $fileProperties.BaseName $ModuleFileContent = Get-Content -Path $Path -ErrorAction Stop $ParserErrors = $null $ParsedFileFunctions = [System.Management.Automation.PSParser]::Tokenize($ModuleFileContent, [ref]$ParserErrors) $ParsedFunctions = ($ParsedFileFunctions | Where-Object { $_.Type -eq "Keyword" -and $_.Content -like 'function' }) $parsedFunction = 0 if ($ParsedFunctions.Count -ge 1) { $FunctionOutputPath = Join-Path -Path $ExtractPath -ChildPath $moduleName if (-not (Test-Path -Path $FunctionOutputPath)) { New-Item $FunctionOutputPath -ItemType 'Directory' } foreach ($Function in $ParsedFunctions) { $parsedFunction++ $FunctionProperties = $ParsedFileFunctions | Where-Object { $_.StartLine -eq $Function.StartLine } $FunctionName = ($FunctionProperties | Where-Object { $_.Type -eq "CommandArgument" }).Content if ($parsedFunction -eq $ParsedFunctions.Count) { $StartLine = ($Function.StartLine) for ($line = $ModuleFileContent.Count; $line -gt $Function.StartLine; $line--) { if ($ModuleFileContent[$line] -like "}") { $EndLine = $line break } } } else { $StartLine = ($Function.StartLine) for ($line = $ParsedFunctions[$parsedFunction].StartLine; $line -gt $Function.StartLine; $line--) { if ($ModuleFileContent[$line] -like "}") { $EndLine = $line break } } } $FunctionOutputFileName = "{0}\{1}{2}" -f $FunctionOutputPath, $FunctionName, ".ps1" if (-not (Test-Path -Path $FunctionOutputFileName)) { Out-File -FilePath $FunctionOutputFileName } for ($line = $StartLine; $line -lt $EndLine; $line++) { Add-Content -Path $FunctionOutputFileName -Value $ModuleFileContent[$line] } } } else { throw "File contains no functions" } } catch { throw } } #EndRegion '.\private\ExportFunctionsFromModule.ps1' 105 #Region '.\private\GetFileContent.ps1' 0 function GetFileContent { <# .SYNOPSIS Gets the content of a script file .DESCRIPTION Gets the content of the file or the content of the function inside the file .PARAMETER Path A file name to parse .EXAMPLE $fileContent = GetFileContent -Path 'c:\file.txt' #> [CmdletBinding()] [OutputType([System.String[]])] param ( [parameter(Mandatory = $true)] [string]$Path ) try { $fileContent = Get-Content -Path $Path $parserErrors = $null if ([string]::IsNullOrEmpty($fileContent)) { $parsedFileFunctions = @() } else { ($parsedFileFunctions, $parserErrors) = GetParsedContent -Content $fileContent } if ($parserErrors -ge 1) { throw "Error parsing file" } $parsedFunctions = @($parsedFileFunctions | Where-Object { $_.Type -eq "Keyword" -and $_.Content -like 'function' }) if ($parsedFunctions.Count -gt 1) { throw "Too many functions in file, file is invalid" } if ($parsedFunctions.Count -eq 0) { for ($line = 0; $line -lt $fileContent.Count; $line++) { $parsedFileContent += $fileContent[$line] $parsedFileContent += "`r`n" } } else { if ($fileContent.Count -gt 1) { foreach ($function in $parsedFunctions) { $startLine = ($function.StartLine) for ($line = $fileContent.Count - 1; $line -gt $function.StartLine; $line--) { if ($fileContent[$line] -like "*}*") { $endLine = $line break } } for ($line = $startLine; $line -lt $endLine; $line++) { $parsedFileContent += $fileContent[$line] if ($line -ne ($fileContent.Count - 1)) { $parsedFileContent += "`r`n" } } } } else { [int]$startBracket = $fileContent.IndexOf('{') [int]$endBracket = $fileContent.LastIndexOf('}') $parsedFileContent = $fileContent.substring($startBracket + 1, $endBracket - 1 - $startBracket) } } } catch { throw } return $parsedFileContent } #EndRegion '.\private\GetFileContent.ps1' 104 #Region '.\private\GetFileList.ps1' 0 function GetFileList { <# .SYNOPSIS Return a list of files .DESCRIPTION Return a list of files from the specified path matching the passed extension .PARAMETER Path A string containing the path .PARAMETER Extension A string containing the extension .PARAMETER Recurse A switch specifying whether or not to recursively search the path specified .PARAMETER IgnoreFile A path to a .psqcignore file (.gitignore file format) for ignoring files .EXAMPLE $files = GetFileList -Path 'c:\folder' -Extension ".ps1" .EXAMPLE $files = GetFileList -Path 'c:\folder' -Extension ".ps1" -Recurse #.EXAMPLE #$files = GetFileList -Path 'c:\folder' -IgnoreFile ".psqcignore" #> [CmdletBinding()] [OutputType([System.Object[]])] param ( [parameter(Mandatory = $true)] [string]$Path, [parameter(Mandatory = $true)] [string]$Extension, [parameter(Mandatory = $false)] [switch]$Recurse, [parameter(Mandatory = $false)] [string]$IgnoreFile ) $Extension = $Extension if (Test-Path -Path $Path) { $FileNameArray = @() if ($PSBoundParameters.ContainsKey('IgnoreFile')) { $SelectedFilesArray = Get-FilteredChildItem -Path $Path -IgnoreFileName $IgnoreFile } else { $gciSplat = @{ 'Path' = $Path 'Exclude' = "*.Tests.*" } if ($PSBoundParameters.ContainsKey('Recurse')) { $gciSplat.Add('Recurse', $true) } $SelectedFilesArray = Get-ChildItem @gciSplat } $SelectedFilesArray | Where-Object { $_.Extension -eq $Extension } | Select-Object -Property FullName | ForEach-Object { $FileNameArray += [string]$_.FullName } } return $FileNameArray } #EndRegion '.\private\GetFileList.ps1' 70 #Region '.\private\GetFunctionCount.ps1' 0 function GetFunctionCount { <# .SYNOPSIS Return the count of functions within Module and its Manifest .DESCRIPTION Return the count of functions in the Module and Manifest and whether they appear in their counterpart. e.g. Whether the functions in the manifest appear in the module and vice versa .PARAMETER ModulePath A string containing the Module filename .PARAMETER ManifestPath A string containing the Manifest filename .EXAMPLE ($ExportedCommandsCount, $CommandFoundInModuleCount, $CommandInModuleCount, $CommandFoundInManifestCount) = GetFunctionCount -ModulePath $ModulePath -ManifestPath $ManifestPath #> [CmdletBinding()] [OutputType([Int[]])] param ( [parameter(Mandatory = $true)] [string]$ModulePath, [parameter(Mandatory = $true)] [string]$ManifestPath ) try { if (Test-Path -Path $ManifestPath) { $ExportedCommands = (Test-ModuleManifest -Path $ManifestPath -ErrorAction Stop).ExportedCommands $ExportedCommandsCount = $ExportedCommands.Count } else { throw "Manifest file doesn't exist" } } catch { $ExportedCommands = @() $ExportedCommandsCount = 0 } try { if (Test-Path -Path $ModulePath) { ($ParsedModule, $ParserErrors) = GetParsedFile -Path $ModulePath } else { throw "Module file doesn't exist" } } catch { $ParsedModule = @() $ParserErrors = 1 } $CommandFoundInModuleCount = 0 $CommandFoundInManifestCount = 0 $CommandInModuleCount = 0 if ( -not ([string]::IsNullOrEmpty($ParsedModule))) { foreach ($ExportedCommand in $ExportedCommands.Keys) { if ( ($ParsedModule | Where-Object { $_.Type -eq "CommandArgument" -and $_.Content -eq $ExportedCommand })) { $CommandFoundInModuleCount++ } } $functionNames = @() $functionKeywords = ($ParsedModule | Where-Object { $_.Type -eq "Keyword" -and $_.Content -eq "function" }) $functionKeywords | ForEach-Object { $functionLineNo = $_.StartLine $functionNames += ($ParsedModule | Where-Object { $_.Type -eq "CommandArgument" -and $_.StartLine -eq $functionLineNo }) } } if ($ExportedCommandsCount -ge 1) { foreach ($function in $functionNames) { $CommandInModuleCount++ if ($ExportedCommands.ContainsKey($function.Content)) { $CommandFoundInManifestCount++ } } } return ($ExportedCommandsCount, $CommandFoundInModuleCount, $CommandInModuleCount, $CommandFoundInManifestCount) } #EndRegion '.\private\GetFunctionCount.ps1' 101 #Region '.\private\GetParsedContent.ps1' 0 function GetParsedContent { <# .SYNOPSIS Get the tokenized content of the passed data .DESCRIPTION Get and return the tokenized content of the passed PowerShell script content .PARAMETER Content A string containing PowerShell script content .EXAMPLE ($ParsedModule, $ParserErrorCount) = GetParsedContent -Content $fileContent #> [CmdletBinding()] [OutputType([System.Object[]], [System.Int32], [System.Void])] param ( [parameter(Mandatory = $true)] [System.Object[]]$Content ) if (-not ([string]::IsNullOrEmpty($Content))) { $ParserErrors = $null $ParsedModule = [System.Management.Automation.PSParser]::Tokenize($Content, [ref]$ParserErrors) $ParserErrorCount = $ParserErrors.Count } else { $ParsedModule = $null $ParserErrorCount = 1 } return $ParsedModule, $ParserErrorCount } #EndRegion '.\private\GetParsedContent.ps1' 40 #Region '.\private\GetParsedFile.ps1' 0 function GetParsedFile { <# .SYNOPSIS Get the tokenized content of the passed file .DESCRIPTION Get and return the tokenized content of the passed PowerShell file .PARAMETER Path A string containing PowerShell filename .EXAMPLE ($ParsedModule, $ParserErrors) = GetParsedFile -Path $ModuleFile #> [CmdletBinding()] [OutputType([System.Object[]])] param ( [parameter(Mandatory = $true)] [string]$Path ) try { if (-not(Test-Path -Path $Path)) { throw "$Path doesn't exist" } } catch { throw $_ } $fileContent = Get-Content -Path $Path -Raw ($ParsedModule, $ParserErrorCount) = GetParsedContent -Content $fileContent return $ParsedModule, $ParserErrorCount } #EndRegion '.\private\GetParsedFile.ps1' 39 #Region '.\private\GetScriptParameter.ps1' 0 function GetScriptParameter { <# .SYNOPSIS Get a list of the parameters in the param block .DESCRIPTION Create a list of the parameters, and their type (if available) from the param block .PARAMETER Content A string containing the text of the script .EXAMPLE $parameterVariables = GetScriptParameter -Content $Content #> [CmdletBinding()] [OutputType([System.Exception], [HashTable])] param ( [parameter(Mandatory = $true)] [String[]]$Content ) try { $parsedScript = [System.Management.Automation.Language.Parser]::ParseInput($Content, [ref]$null, [ref]$null) if ([string]::IsNullOrEmpty($parsedScript.ParamBlock)) { throw "No parameters found" } [string]$paramBlock = $parsedScript.ParamBlock ($ParsedContent, $ParserErrorCount) = GetParsedContent -Content $paramBlock $paramBlockArray = ($paramBlock -split '\n').Trim() $parametersFound = @{} for ($line = 0; $line -le $paramBlockArray.Count; $line++) { $paramToken = @($ParsedContent | Where-Object { $_.StartLine -eq $line }) foreach ($token in $paramToken) { if ($token.Type -eq 'Attribute' -and $token.Content -eq "Parameter") { break } if ($token.Type -eq 'Type') { $foundType = $token.Content } if ($token.Type -eq 'Variable') { $parametersFound[$token.Content] = $foundType $foundType = $null break } } } return $parametersFound } catch { throw $_.Exception.Message } } #EndRegion '.\private\GetScriptParameter.ps1' 77 #Region '.\private\GetTagList.ps1' 0 function GetTagList { <# .SYNOPSIS Return a list of test tags .DESCRIPTION Return a list of test tags from the module and script checks file .EXAMPLE ($moduleTags, $scriptTags) = GetTagList #> [CmdletBinding()] [OutputType([System.Object[]])] param ( ) $moduleTags = @() $scriptTags = @() $modulePath = (Get-Module -Name 'PSQualityCheck').ModuleBase $checksPath = (Join-Path -Path $modulePath -ChildPath "data") Get-Content -Path (Join-Path -Path $checksPath -ChildPath "Module.Tests.ps1") -Raw | ForEach-Object { $ast = [Management.Automation.Language.Parser]::ParseInput($_, [ref]$null, [ref]$null) $ast.FindAll( { param($node) $node -is [System.Management.Automation.Language.CommandAst] -and $node.CommandElements[0].Value -eq "Describe" -and $node.CommandElements[2] -is [System.Management.Automation.Language.CommandParameterAst] -and $node.CommandElements[2].ParameterName -eq "Tag" }, $true) | ForEach-Object { $moduleTags += $_.CommandElements[3].Value } $ast.FindAll( { param($node) $node -is [System.Management.Automation.Language.CommandAst] -and $node.CommandElements[0].Value -eq "It" -and $node.CommandElements[2] -is [System.Management.Automation.Language.CommandParameterAst] -and $node.CommandElements[2].ParameterName -eq "Tag" }, $true) | ForEach-Object { $moduleTags += $_.CommandElements[3].Value } } Get-Content -Path (Join-Path -Path $checksPath -ChildPath "Script.Tests.ps1") -Raw | ForEach-Object { $ast = [Management.Automation.Language.Parser]::ParseInput($_, [ref]$null, [ref]$null) $ast.FindAll( { param($node) $node -is [System.Management.Automation.Language.CommandAst] -and $node.CommandElements[0].Value -eq "Describe" -and $node.CommandElements[2] -is [System.Management.Automation.Language.CommandParameterAst] -and $node.CommandElements[2].ParameterName -eq "Tag" }, $true) | ForEach-Object { $scriptTags += $_.CommandElements[3].Value } $ast.FindAll( { param($node) $node -is [System.Management.Automation.Language.CommandAst] -and $node.CommandElements[0].Value -eq "It" -and $node.CommandElements[2] -is [System.Management.Automation.Language.CommandParameterAst] -and $node.CommandElements[2].ParameterName -eq "Tag" }, $true) | ForEach-Object { $scriptTags += $_.CommandElements[3].Value } } return $moduleTags, $scriptTags } #EndRegion '.\private\GetTagList.ps1' 72 #Region '.\private\GetToken.ps1' 0 function GetToken { <# .SYNOPSIS Get token(s) from the tokenized output .DESCRIPTION Get token(s) from the tokenized output matching the passed Type and Content .PARAMETER ParsedContent A string array containing the Tokenized data .PARAMETER Type The token type to be found .PARAMETER Content The token content (or value) to be found .EXAMPLE $outputTypeToken = (GetToken -ParsedContent $ParsedFile -Type "Attribute" -Content "OutputType") #> [CmdletBinding()] [OutputType([System.Object[]])] param ( [parameter(Mandatory = $true)] [System.Object[]]$ParsedContent, [parameter(Mandatory = $true)] [string]$Type, [parameter(Mandatory = $true)] [string]$Content ) $token = GetTokenMarker -ParsedContent $ParsedContent -Type $Type -Content $Content $tokens = GetTokenComponent -ParsedContent $ParsedContent -StartLine $token.StartLine return $tokens } #EndRegion '.\private\GetToken.ps1' 39 #Region '.\private\GetTokenComponent.ps1' 0 function GetTokenComponent { <# .SYNOPSIS Get all the tokens components from a single line .DESCRIPTION Get all the tokens components from a single line in the tokenized content .PARAMETER ParsedContent A string array containing the tokenized content .PARAMETER StartLine A integer of the starting line to parse .EXAMPLE $tokens = GetTokenComponent -ParsedContent $ParsedContent -StartLine 10 #> [CmdletBinding()] [OutputType([System.Object[]])] param ( [parameter(Mandatory = $true)] [System.Object[]]$ParsedContent, [parameter(Mandatory = $true)] [int]$StartLine ) #* This is just to satisfy the PSScriptAnalyzer #* which can't find the variables in the 'Where-Object' clause (even though it's valid) $StartLine = $StartLine $tokenComponents = @($ParsedContent | Where-Object { $_.StartLine -eq $StartLine }) return $tokenComponents } #EndRegion '.\private\GetTokenComponent.ps1' 36 #Region '.\private\GetTokenMarker.ps1' 0 function GetTokenMarker { <# .SYNOPSIS Gets token from the tokenized output .DESCRIPTION Gets single token from the tokenized output matching the passed Type and Content .PARAMETER ParsedContent A string array containing the Tokenized data .PARAMETER Type The token type to be found .PARAMETER Content The token content (or value) to be found .EXAMPLE $token = GetTokenMarker -ParsedContent $ParsedContent -Type $Type -Content $Content #> [CmdletBinding()] [OutputType([System.Object[]])] param ( [parameter(Mandatory = $true)] [System.Object[]]$ParsedContent, [parameter(Mandatory = $true)] [string]$Type, [parameter(Mandatory = $true)] [string]$Content ) #* This is just to satisfy the PSScriptAnalyzer #* which can't find the variables in the 'Where-Object' clause (even though it's valid) $Type = $Type $Content = $Content $token = @($ParsedContent | Where-Object { $_.Type -eq $Type -and $_.Content -eq $Content }) return $token } #EndRegion '.\private\GetTokenMarker.ps1' 42 #Region '.\private\TestHelpTokensCountisValid.ps1' 0 function TestHelpTokensCountIsValid { <# .SYNOPSIS Check that help tokens count is valid .DESCRIPTION Check that the help tokens count is valid by making sure that they appear between Min and Max times .PARAMETER HelpTokens A array of tokens containing the tokens of the Help Comment .PARAMETER HelpRulesPath Path to the HelpRules file .EXAMPLE TestHelpTokensCountIsValid -HelpTokens $HelpTokens -HelpRulesPath "C:\HelpRules" .NOTES This function will only check the Min/Max counts of required help tokens #> [CmdletBinding()] [OutputType([System.Exception], [System.Void])] param ( [parameter(Mandatory = $true)] [HashTable]$HelpTokens, [parameter(Mandatory = $true)] [string]$HelpRulesPath ) try { $helpRules = Import-PowerShellDataFile -Path $HelpRulesPath $tokenFound = @{} for ($order = 1; $order -le $HelpRules.Count; $order++) { $helpRuleIndex = [string]$order $token = $HelpRules.$helpRuleIndex.Key $tokenFound[$token] = $false } $tokenErrors = @() foreach ($key in $HelpTokens.Keys) { for ($order = 1; $order -le $HelpRules.Count; $order++) { $helpRuleIndex = [string]$order $token = $HelpRules.$helpRuleIndex if ( $token.Key -eq $key ) { $tokenFound[$key] = $true if ($HelpTokens.$key.Count -lt $token.MinOccurrences -or $HelpTokens.$key.Count -gt $token.MaxOccurrences -and $token.Required -eq $true) { $tokenErrors += "Found $(($HelpTokens.$key).Count) occurrences of '$key' which is not between $($token.MinOccurrences) and $($token.MaxOccurrences). " } } } } if ($tokenErrors.Count -ge 1) { throw $tokenErrors } } catch { throw $_.Exception.Message } } #EndRegion '.\private\TestHelpTokensCountisValid.ps1' 83 #Region '.\private\TestHelpTokensParamsMatch.ps1' 0 function TestHelpTokensParamsMatch { <# .SYNOPSIS Checks to see whether the parameters and help PARAMETER statements match .DESCRIPTION Checks to see whether the parameters in the param block and in the help PARAMETER statements exist in both locations .PARAMETER HelpTokens A array of tokens containing the tokens of the Help Comment .PARAMETER ParameterVariables A object containing the parameters from the param block .EXAMPLE TestHelpTokensParamsMatch -HelpTokens $HelpTokens -ParameterVariables $ParameterVariables #> [CmdletBinding()] [OutputType([System.Exception], [System.String[]])] param ( [parameter(Mandatory = $true)] [HashTable]$HelpTokens, [parameter(Mandatory = $true)] [PSCustomObject]$ParameterVariables ) try { $foundInHelpErrors = @() $foundInParamErrors = @() foreach ($key in $ParameterVariables.Keys) { $foundInHelp = $false foreach ($token in $HelpTokens.".PARAMETER") { if ($key -eq $token.Name) { $foundInHelp = $true break } } if ($foundInHelp -eq $false) { $foundInHelpErrors += "Parameter block variable '$key' was not found in help. " } } foreach ($token in $HelpTokens.".PARAMETER") { $foundInParams = $false foreach ($key in $ParameterVariables.Keys) { if ($key -eq $token.Name) { $foundInParams = $true break } } if ($foundInParams -eq $false) { $foundInParamErrors += "Help defined variable '$($token.Name)' was not found in parameter block definition. " } } if ($foundInHelpErrors.Count -ge 1 -or $foundInParamErrors.Count -ge 1) { $allErrors = $foundInHelpErrors + $foundInParamErrors throw $allErrors } } catch { throw $_.Exception.Message } } #EndRegion '.\private\TestHelpTokensParamsMatch.ps1' 93 #Region '.\private\TestHelpTokensTextIsValid.ps1' 0 function TestHelpTokensTextIsValid { <# .SYNOPSIS Check that Help Tokens text is valid .DESCRIPTION Check that the Help Tokens text is valid by making sure that they its not empty .PARAMETER HelpTokens A array of tokens containing the tokens of the Help Comment .EXAMPLE TestHelpTokensTextIsValid -HelpTokens $HelpTokens #> [CmdletBinding()] [OutputType([System.Exception], [System.Void])] param ( [parameter(Mandatory = $true)] [HashTable]$HelpTokens ) try { $tokenErrors = @() foreach ($key in $HelpTokens.Keys) { $tokenCount = @($HelpTokens.$key) for ($loop = 0; $loop -lt $tokenCount.Count; $loop++) { $token = $HelpTokens.$key[$loop] if ([string]::IsNullOrWhitespace($token.Text)) { $tokenErrors += "Found '$key' does not have any text. " } } } if ($tokenErrors.Count -ge 1) { throw $tokenErrors } } catch { throw $_.Exception.Message } } #EndRegion '.\private\TestHelpTokensTextIsValid.ps1' 58 #Region '.\private\TestImportModuleIsValid.ps1' 0 function TestImportModuleIsValid { <# .SYNOPSIS Test that the Import-Module commands are valid .DESCRIPTION Test that the Import-Module commands contain a -Name parameter, and one of RequiredVersion, MinimumVersion or MaximumVersion .PARAMETER ParsedContent An object containing the source file parsed into its Tokenizer components .PARAMETER ImportModuleTokens An object containing the Import-Module tokens found .EXAMPLE TestImportModuleIsValid -ParsedContent $ParsedContent -ImportModuleTokens $importModuleTokens #> [CmdletBinding()] [OutputType([System.Exception], [System.Void])] param ( [parameter(Mandatory = $true)] [System.Object[]]$ParsedContent, [parameter(Mandatory = $true)] [System.Object[]]$ImportModuleTokens ) try { $errString = "" foreach ($token in $importModuleTokens) { $importModuleStatement = GetTokenComponent -ParsedContent $ParsedContent -StartLine $token.StartLine try { $name = ($importModuleStatement | Where-Object { $_.Type -eq "CommandArgument" } | Select-Object -First 1).Content } catch { $name = $null } if ($null -eq $name) { $name = ($importModuleStatement | Where-Object { $_.Type -eq "String" } | Select-Object -First 1).Content } if (-not($importModuleStatement | Where-Object { $_.Type -eq "CommandParameter" -and $_.Content -eq "-Name" })) { $errString += "Import-Module for '$name' : Missing -Name parameter keyword. " } if (-not($importModuleStatement | Where-Object { $_.Type -eq "CommandParameter" -and ( $_.Content -eq "-RequiredVersion" -or $_.Content -eq "-MinimumVersion" -or $_.Content -eq "-MaximumVersion" ) })) { $errString += "Import-Module for '$name' : Missing -RequiredVersion, -MinimumVersion or -MaximumVersion parameter keyword. " } } if (-not ([string]::IsNullOrEmpty($errString))) { throw $errString } } catch { throw $_.Exception.Message } } #EndRegion '.\private\TestImportModuleIsValid.ps1' 77 #Region '.\private\TestParameterVariablesHaveType.ps1' 0 function TestParameterVariablesHaveType { <# .SYNOPSIS Check that all the passed parameters have a type variable set. .DESCRIPTION Check that all the passed parameters have a type variable set. .PARAMETER ParameterVariables A HashTable containing the parameters from the param block .EXAMPLE TestParameterVariablesHaveType -ParameterVariables $ParameterVariables #> [CmdletBinding()] [OutputType([System.Exception], [System.Void])] param ( [parameter(Mandatory = $true)] [HashTable]$ParameterVariables ) $variableErrors = @() try { foreach ($key in $ParameterVariables.Keys) { if ([string]::IsNullOrEmpty($ParameterVariables.$key)) { $variableErrors += "Parameter '$key' does not have a type defined." } } if ($variableErrors.Count -ge 1) { throw $variableErrors } } catch { throw $_.Exception.Message } } #EndRegion '.\private\TestParameterVariablesHaveType.ps1' 49 #Region '.\private\TestRequiredToken.ps1' 0 function TestRequiredToken { <# .SYNOPSIS Check that help tokens contain required tokens .DESCRIPTION Check that the help comments contain tokens that are specified in the external verification data file .PARAMETER HelpTokens A array of tokens containing the tokens of the Help Comment .PARAMETER HelpRulesPath Path to the HelpRules file .EXAMPLE TestRequiredToken -HelpTokens $HelpTokens -HelpRulesPath "C:\HelpRules" #> [CmdletBinding()] [OutputType([System.Exception], [System.Void])] param ( [parameter(Mandatory = $true)] [HashTable]$HelpTokens, [parameter(Mandatory = $true)] [string]$HelpRulesPath ) try { $helpRules = Import-PowerShellDataFile -Path $HelpRulesPath $tokenErrors = @() for ($order = 1; $order -le $HelpRules.Count; $order++) { $helpRuleIndex = [string]$order $token = $HelpRules.$helpRuleIndex if ($token.Key -notin $HelpTokens.Keys ) { if ($token.Required -eq $true) { $tokenErrors += $token.Key } } } if ($tokenErrors.Count -ge 1) { throw "Missing required token(s): $tokenErrors" } } catch { throw $_.Exception.Message } } #EndRegion '.\private\TestRequiredToken.ps1' 63 #Region '.\private\TestUnspecifiedToken.ps1' 0 function TestUnspecifiedToken { <# .SYNOPSIS Check that help tokens do not contain unspecified tokens .DESCRIPTION Check that the help comments do not contain tokens that are not specified in the external verification data file .PARAMETER HelpTokens A array of tokens containing the tokens of the Help Comment .PARAMETER HelpRulesPath Path to the HelpRules file .EXAMPLE TestUnspecifiedToken -HelpTokens $HelpTokens -HelpRulesPath "C:\HelpRules" #> [CmdletBinding()] [OutputType([System.Exception], [System.Void])] param ( [parameter(Mandatory = $true)] [HashTable]$HelpTokens, [parameter(Mandatory = $true)] [string]$HelpRulesPath ) try { $helpRules = Import-PowerShellDataFile -Path $HelpRulesPath $tokenErrors = @() $helpTokensKeys = @() for ($order = 1; $order -le $helpRules.Count; $order++) { $helpRuleIndex = [string]$order $token = $helpRules.$helpRuleIndex $helpTokensKeys += $token.key } foreach ($key in $helpTokens.Keys) { if ( $key -notin $helpTokensKeys ) { $tokenErrors += $key } } if ($tokenErrors.Count -ge 1) { throw "Found extra, non-specified, token(s): $tokenErrors" } } catch { throw $_.Exception.Message } } #EndRegion '.\private\TestUnspecifiedToken.ps1' 66 #Region '.\public\Invoke-PSQualityCheck.ps1' 0 function Invoke-PSQualityCheck { <# .SYNOPSIS Invoke the PSQualityCheck tests .DESCRIPTION Invoke a series of Pester-based quality tests on the passed files .PARAMETER Path A string array containing paths to check for testable files .PARAMETER File A string array containing testable files .PARAMETER Recurse A switch specifying whether or not to recursively search the path specified .PARAMETER ScriptAnalyzerRulesPath A path the the external PSScriptAnalyzer rules .PARAMETER ShowCheckResults Show a summary of the Check results at the end of processing Note: this cannot be used with -Passthru .PARAMETER ExportCheckResults Exports the Check results at the end of processing to file .PARAMETER Passthru Returns the Check results objects back to the caller Note: this cannot be used with -ShowCheckResults .PARAMETER PesterConfiguration A Pester configuration object to allow configuration of Pester .PARAMETER Include An array of test tags to run .PARAMETER Exclude An array of test tags to not run .PARAMETER ProjectPath A path to the root of a Project .PARAMETER HelpRulesPath A path to the HelpRules parameter file .PARAMETER IgnoreFile A path to the .psqcignore file which excludes files/path from the tests. This is in the .gitignore format .EXAMPLE Invoke-PSQualityCheck -Path 'C:\Scripts' This will call the quality checks on single path .EXAMPLE Invoke-PSQualityCheck -Path 'C:\Scripts' -Recurse This will call the quality checks on single path and sub folders .EXAMPLE Invoke-PSQualityCheck -Path @('C:\Scripts', 'C:\MoreScripts') This will call the quality checks with multiple paths .EXAMPLE Invoke-PSQualityCheck -ProjectPath 'C:\Project' -IgnoreFile ".psqcignore" This will call the project quality checks on the C:\Project folder with the .psqcignore file .EXAMPLE Invoke-PSQualityCheck -File 'C:\Scripts\Script.ps1' This will call the quality checks with single script file .EXAMPLE Invoke-PSQualityCheck -File 'C:\Scripts\Script.psm1' This will call the quality checks with single module file .EXAMPLE Invoke-PSQualityCheck -File 'C:\Scripts\Script.psd1' This will call the quality checks with single datafile file Note: The datafile test will fail as it is not a file that is accepted for testing .EXAMPLE Invoke-PSQualityCheck -File @('C:\Scripts\Script.ps1','C:\Scripts\Script2.ps1') This will call the quality checks with multiple files. Files can be either scripts or modules .EXAMPLE Invoke-PSQualityCheck -File 'C:\Scripts\Script.ps1' -ScriptAnalyzerRulesPath 'C:\ScriptAnalyzerRulesPath' This will call the quality checks with single file and the extra PSScriptAnalyzer rules .EXAMPLE Invoke-PSQualityCheck -Path 'C:\Scripts' -ShowCheckResults This will display a summary of the checks performed (example below uses sample data): Name Files Tested Total Passed Failed Skipped ---- ------------ ----- ------ ------ ------- Module Tests 2 14 14 0 0 Extracting functions 2 2 2 0 0 Extracted function script tests 22 330 309 0 21 Total 24 346 325 0 21 For those who have spotted that the Total files tested isn't a total of the rows above, this is because the Module Tests and Extracting function Tests operate on the same file and are not counted twice .LINK Website: https://github.com/andrewrdavidson/PSQualityCheck SonarQube rules are available here: https://github.com/indented-automation/ScriptAnalyzerRules #> [CmdletBinding()] [OutputType([System.Void], [HashTable], [System.Object[]])] param ( [Parameter(Mandatory = $true, ParameterSetName = "Path")] [String[]]$Path, [Parameter(Mandatory = $true, ParameterSetName = "File")] [String[]]$File, [Parameter(Mandatory = $true, ParameterSetName = "ProjectPath")] [String]$ProjectPath, [Parameter(Mandatory = $false, ParameterSetName = "Path")] [switch]$Recurse, [Parameter(Mandatory = $false)] [String[]]$ScriptAnalyzerRulesPath, [switch]$ShowCheckResults, [switch]$ExportCheckResults, [switch]$Passthru, [Parameter(Mandatory = $false)] [System.Object]$PesterConfiguration, [Parameter(Mandatory = $false)] [String[]]$Include, [Parameter(Mandatory = $false)] [String[]]$Exclude, [Parameter(Mandatory = $false)] [String]$HelpRulesPath, [Parameter(Mandatory = $false)] [String]$IgnoreFile ) Set-StrictMode -Version Latest # External Modules Import-Module -Name 'Pester' -MinimumVersion '5.1.0' -Force Import-Module -Name 'PSScriptAnalyzer' -MinimumVersion '1.19.1' -Force $modulePath = (Get-Module -Name 'PSQualityCheck').ModuleBase # Validate any incoming parameters for clashes if ($PSBoundParameters.ContainsKey('ShowCheckResults') -and $PSBoundParameters.ContainsKey('Passthru')) { Write-Error "-ShowCheckResults and -Passthru cannot be used at the same time" break } $scriptsToTest = @() $modulesToTest = @() $projectResults = $null $moduleResults = $null $extractionResults = $null $extractedScriptResults = $null $scriptResults = $null if ($PSBoundParameters.ContainsKey('HelpRulesPath')) { if ( -not (Test-Path -Path $HelpRulesPath)) { Write-Error "-HelpRulesPath does not exist" break } } else { $helpRulesPath = (Join-Path -Path $modulePath -ChildPath "Data\HelpRules.psd1") } if ($PSBoundParameters.ContainsKey('PesterConfiguration') -and $PesterConfiguration -is [PesterConfiguration]) { # left here so that we can over-ride passed in object with values we require } else { # Default Pester Parameters $PesterConfiguration = [PesterConfiguration]::Default $PesterConfiguration.Run.Exit = $false $PesterConfiguration.CodeCoverage.Enabled = $false $PesterConfiguration.Output.Verbosity = 'Detailed' $PesterConfiguration.Run.PassThru = $true $PesterConfiguration.Should.ErrorAction = 'Stop' } # Analyse the incoming Path and File parameters and produce a list of Modules and Scripts if ($PSBoundParameters.ContainsKey('Path') -or $PSBoundParameters.ContainsKey('ProjectPath')) { if ($PSBoundParameters.ContainsKey('ProjectPath')) { if (Test-Path -Path $ProjectPath) { $container1 = New-PesterContainer -Path (Join-Path -Path $modulePath -ChildPath 'Data\Project.Checks.ps1') -Data @{ Path = $ProjectPath } $PesterConfiguration.Run.Container = $container1 $projectResults = Invoke-Pester -Configuration $PesterConfiguration # setup the rest of the Path based tests # this needs to include the module/public module/private and scripts folder only, not the data or any other folders $ProjectPath = (Resolve-Path -Path $ProjectPath) $sourceRootPath = (Join-Path -Path $ProjectPath -ChildPath "Source") $scriptPath = (Join-Path -Path $ProjectPath -ChildPath "Scripts") $Path = @($scriptPath) # Get the list of Modules $modules = Get-ChildItem -Path $sourceRootPath -Directory foreach ($module in $modules) { $Path += (Join-Path -Path $module -ChildPath "Public") $Path += (Join-Path -Path $module -ChildPath "Private") } } else { Write-Error "Project Path $ProjectPath does not exist" $projectResults = @{ 'Count' = 0 'TotalCount' = 0 'NotRunCount' = 0 'PassedCount' = 0 'FailedCount' = 0 'SkippedCount' = 0 } } } if ($Path -isnot [string[]]) { $Path = @($Path) } foreach ($item in $Path) { # Test whether the item is a directory (also tells us if it exists) if (Test-Path -Path $item -PathType Container) { $getFileListSplat = @{ 'Path' = $item } if ($PSBoundParameters.ContainsKey('IgnoreFile')) { $getFileListSplat.Add('IgnoreFile', (Resolve-Path -Path $IgnoreFile)) } else { if ($PSBoundParameters.ContainsKey('Recurse') -or $PSBoundParameters.ContainsKey('ProjectPath')) { $getFileListSplat.Add('Recurse', $true) } } $scriptsToTest += GetFileList @getFileListSplat -Extension '.ps1' $modulesToTest += GetFileList @getFileListSplat -Extension '.psm1' } else { Write-Warning -Message "$item is not a directory, skipping" } } } if ($PSBoundParameters.ContainsKey('File')) { if ($File -isnot [string[]]) { $File = @($File) } foreach ($item in $File) { # Test whether the item is a file (also tells us if it exists) if (Test-Path -Path $item -PathType Leaf) { $getFilteredItemSplat = @{ 'Path' = $item } if ($PSBoundParameters.ContainsKey('IgnoreFile')) { $getFilteredItemSplat.Add('IgnoreFile', (Resolve-Path -Path $IgnoreFile)) } $itemProperties = Get-FilteredChildItem @getFilteredItemSplat switch ($itemProperties.Extension) { '.psm1' { $modulesToTest += $itemProperties } '.ps1' { $scriptsToTest += $itemProperties } } } else { Write-Warning -Message "$item is not a file, skipping" } } } # Get the list of test tags from the Data files if ($PSBoundParameters.ContainsKey('Include') -or $PSBoundParameters.ContainsKey('Exclude')) { ($moduleTags, $scriptTags) = GetTagList $moduleTagsToInclude = @() $moduleTagsToExclude = @() $scriptTagsToInclude = @() $scriptTagsToExclude = @() $runModuleCheck = $false $runScriptCheck = $false } else { $runModuleCheck = $true $runScriptCheck = $true } if ($PSBoundParameters.ContainsKey('Include')) { if ($Include -eq 'All') { $moduleTagsToInclude = $moduleTags $scriptTagsToInclude = $scriptTags $runModuleCheck = $true $runScriptCheck = $true } else { # Validate tests to include from $Include $Include | ForEach-Object { if ($_ -in $moduleTags) { $moduleTagsToInclude += $_ $runModuleCheck = $true #* To satisfy PSScriptAnalyzer $runModuleCheck = $runModuleCheck $runScriptCheck = $runScriptCheck } } $Include | ForEach-Object { if ($_ -in $scriptTags) { $scriptTagsToInclude += $_ $runScriptCheck = $true #* To satisfy PSScriptAnalyzer $runModuleCheck = $runModuleCheck $runScriptCheck = $runScriptCheck } } } $PesterConfiguration.Filter.Tag = $moduleTagsToInclude + $scriptTagsToInclude } if ($PSBoundParameters.ContainsKey('Exclude')) { # Validate tests to exclude from $Exclude $Exclude | ForEach-Object { if ($_ -in $moduleTags) { $moduleTagsToExclude += $_ $runModuleCheck = $true #* To satisfy PSScriptAnalyzer $runModuleCheck = $runModuleCheck $runScriptCheck = $runScriptCheck } } $Exclude | ForEach-Object { if ($_ -in $scriptTags) { $scriptTagsToExclude += $_ $runScriptCheck = $true #* To satisfy PSScriptAnalyzer $runModuleCheck = $runModuleCheck $runScriptCheck = $runScriptCheck } } $PesterConfiguration.Filter.ExcludeTag = $moduleTagsToExclude + $scriptTagsToExclude } if ($modulesToTest.Count -ge 1) { # Location of files extracted from any passed modules $extractPath = Join-Path -Path ([IO.Path]::GetTempPath()) -ChildPath (New-Guid).Guid if ($runModuleCheck -eq $true) { # Run the Module tests on all the valid module files found $container1 = New-PesterContainer -Path (Join-Path -Path $modulePath -ChildPath 'Data\Module.Checks.ps1') -Data @{ Source = $modulesToTest } $PesterConfiguration.Run.Container = $container1 $moduleResults = Invoke-Pester -Configuration $PesterConfiguration # Extract all the functions from the modules into individual .ps1 files ready for testing $container2 = New-PesterContainer -Path (Join-Path -Path $modulePath -ChildPath 'Data\Extraction.ps1') -Data @{ Source = $modulesToTest; ExtractPath = $extractPath } $PesterConfiguration.Run.Container = $container2 $extractionResults = Invoke-Pester -Configuration $PesterConfiguration } else { $moduleResults = @{ 'Count' = 0 'TotalCount' = 0 'NotRunCount' = 0 'PassedCount' = 0 'FailedCount' = 0 'SkippedCount' = 0 } $extractionResults = @{ 'Count' = 0 'TotalCount' = 0 'NotRunCount' = 0 'PassedCount' = 0 'FailedCount' = 0 'SkippedCount' = 0 } } if ($runScriptCheck -eq $true -and (Test-Path -Path $extractPath -ErrorAction SilentlyContinue)) { # Get a list of the 'extracted' function scripts .ps1 files $extractedScriptsToTest = Get-ChildItem -Path $extractPath -Include '*.ps1' -Recurse # Run the Script tests against all the extracted functions .ps1 files $container3 = New-PesterContainer -Path (Join-Path -Path $modulePath -ChildPath 'Data\Script.Checks.ps1') -Data @{ Source = $extractedScriptsToTest; ScriptAnalyzerRulesPath = $ScriptAnalyzerRulesPath; HelpRulesPath = $HelpRulesPath } $PesterConfiguration.Run.Container = $container3 $extractedScriptResults = Invoke-Pester -Configuration $PesterConfiguration } else { $extractedScriptResults = @{ 'Count' = 0 'TotalCount' = 0 'NotRunCount' = 0 'PassedCount' = 0 'FailedCount' = 0 'SkippedCount' = 0 } } # Tidy up and temporary paths that have been used if ( Test-Path -Path $ExtractPath -ErrorAction SilentlyContinue) { Get-ChildItem -Path $ExtractPath -Recurse -ErrorAction SilentlyContinue | Remove-Item -Force -Recurse Remove-Item $ExtractPath -Force -ErrorAction SilentlyContinue } } if ($scriptsToTest.Count -ge 1 -and $runScriptCheck -eq $true) { # Run the Script tests against all the valid script files found $container3 = New-PesterContainer -Path (Join-Path -Path $modulePath -ChildPath 'Data\Script.Checks.ps1') -Data @{ Source = $scriptsToTest; ScriptAnalyzerRulesPath = $ScriptAnalyzerRulesPath } $PesterConfiguration.Run.Container = $container3 $scriptResults = Invoke-Pester -Configuration $PesterConfiguration } else { $scriptResults = @{ 'Count' = 0 'TotalCount' = 0 'NotRunCount' = 0 'PassedCount' = 0 'FailedCount' = 0 'SkippedCount' = 0 } } # Show/Export results in the various formats if ($PSBoundParameters.ContainsKey('ShowCheckResults')) { $qualityCheckResults = @() $filesTested = $total = $passed = $failed = $skipped = 0 if ($null -ne $projectResults) { $qualityCheckResults += @{ 'Test' = 'Project Tests' 'Files Tested' = 0 'Total' = ($projectResults.TotalCount - $projectResults.NotRunCount) 'Passed' = $projectResults.PassedCount 'Failed' = $projectResults.FailedCount 'Skipped' = $projectResults.SkippedCount } $filesTested += 0 $total += ($projectResults.TotalCount - $projectResults.NotRunCount) $passed += $projectResults.PassedCount $failed += $projectResults.FailedCount $skipped += $projectResults.SkippedCount } if ($null -ne $moduleResults) { $qualityCheckResults += @{ 'Test' = 'Module Tests' 'Files Tested' = $ModulesToTest.Count 'Total' = ($moduleResults.TotalCount - $moduleResults.NotRunCount) 'Passed' = $moduleResults.PassedCount 'Failed' = $moduleResults.FailedCount 'Skipped' = $moduleResults.SkippedCount } $filesTested += $ModulesToTest.Count $total += ($moduleResults.TotalCount - $moduleResults.NotRunCount) $passed += $moduleResults.PassedCount $failed += $moduleResults.FailedCount $skipped += $moduleResults.SkippedCount } if ($null -ne $extractionResults) { $qualityCheckResults += @{ 'Test' = 'Extracting functions' 'Files Tested' = $ModulesToTest.Count 'Total' = ($extractionResults.TotalCount - $extractionResults.NotRunCount) 'Passed' = $extractionResults.PassedCount 'Failed' = $extractionResults.FailedCount 'Skipped' = $extractionResults.SkippedCount } $total += ($extractionResults.TotalCount - $extractionResults.NotRunCount) $passed += $extractionResults.PassedCount $failed += $extractionResults.FailedCount $skipped += $extractionResults.SkippedCount } if ($null -ne $extractedScriptResults) { $qualityCheckResults += @{ 'Test' = 'Extracted function script tests' 'Files Tested' = $extractedScriptsToTest.Count 'Total' = ($extractedScriptResults.TotalCount - $extractedScriptResults.NotRunCount) 'Passed' = $extractedScriptResults.PassedCount 'Failed' = $extractedScriptResults.FailedCount 'Skipped' = $extractedScriptResults.SkippedCount } $filesTested += $extractedScriptsToTest.Count $total += ($extractedScriptResults.TotalCount - $extractedScriptResults.NotRunCount) $passed += $extractedScriptResults.PassedCount $failed += $extractedScriptResults.FailedCount $skipped += $extractedScriptResults.SkippedCount } if ($null -ne $scriptResults) { $qualityCheckResults += @{ 'Test' = "Script Tests" 'Files Tested' = $scriptsToTest.Count 'Total' = ($scriptResults.TotalCount - $scriptResults.NotRunCount) 'Passed' = $scriptResults.PassedCount 'Failed' = $scriptResults.FailedCount 'Skipped' = $scriptResults.SkippedCount } $filesTested += $scriptsToTest.Count $total += ($scriptResults.TotalCount - $scriptResults.NotRunCount) $passed += $scriptResults.PassedCount $failed += $scriptResults.FailedCount $skipped += $scriptResults.SkippedCount } $qualityCheckResults += @{ 'Test' = "Total" 'Files Tested' = $filesTested 'Total' = $total 'Passed' = $passed 'Failed' = $failed 'Skipped' = $skipped } # This works on PS5 and PS7 $qualityCheckResults | ForEach-Object { [PSCustomObject]@{ 'Test' = $_.Test 'Files Tested' = $_.'Files Tested' 'Total' = $_.total 'Passed' = $_.passed 'Failed' = $_.failed 'Skipped' = $_.skipped } } | Format-Table -AutoSize # This works on PS7 not on PS5 # $qualityCheckResults | Select-Object Name, 'Files Tested', Total, Passed, Failed, Skipped | Format-Table -AutoSize } if ($PSBoundParameters.ContainsKey('ExportCheckResults')) { $projectResults | Export-Clixml -Path "projectResults.xml" $moduleResults | Export-Clixml -Path "moduleResults.xml" $extractionResults | Export-Clixml -Path "extractionResults.xml" $scriptResults | Export-Clixml -Path "scriptsToTest.xml" $extractedScriptResults | Export-Clixml -Path "extractedScriptResults.xml" } if ($PSBoundParameters.ContainsKey('Passthru')) { if ($PesterConfiguration.Run.PassThru.Value -eq $true) { $resultObject = @{ 'project' = $projectResults 'module' = $moduleResults 'extraction' = $extractionResults 'script' = $scriptResults 'extractedscript' = $extractedScriptResults } return $resultObject } else { Write-Error "Unable to pass back result objects. Passthru not enabled in Pester Configuration object" } } } #EndRegion '.\public\Invoke-PSQualityCheck.ps1' 653 |