resources/PesterTests/QualityAssurance/QA.Tests.ps1
|
BeforeDiscovery { $data = Get-MAProjectInfo $script:classFiles = Get-ChildItem -Path $data.ClassesDir -Filter '*.ps1' $script:files = @( Get-ChildItem -Path $data.PublicDir -Filter '*.ps1' Get-ChildItem -Path $data.PrivateDir -Filter '*.ps1' ) } BeforeAll { $script:ScriptAnalyzerSettings = [System.IO.Path]::Combine($PSScriptRoot, '..', '..', 'PSScriptAnalyzerSettings.psd1') $script:data = Get-MAProjectInfo $script:psmPresent = Test-Path -Path $data.ModuleFilePSM1 $script:psdPresent = Test-Path -Path $data.ManifestFilePSD1 $script:publicFunctions = Get-ChildItem -Path $script:data.PublicDir -Filter '*.ps1' $script:privateFunctions = Get-ChildItem -Path $script:data.PrivateDir -Filter '*.ps1' } Describe 'Class File: <_.BaseName>' -ForEach $classFiles -AllowNullOrEmptyForEach -Tag 'FunctionQA' { Context 'Code Quality Check' { It 'is valid PowerShell Code' { $psFile = Get-Content -Path $_ -ErrorAction Stop $errors = $null $null = [System.Management.Automation.PSParser]::Tokenize($psFile, [ref]$errors) $errors.Count | Should-Be 0 } It 'passes ScriptAnalyzer' { $saResults = Invoke-ScriptAnalyzer -Path $_ -Settings $ScriptAnalyzerSettings @($saResults).Count | Should-Be 0 -Because $($saResults.Message -join ';') } } } Describe 'File: <_.BaseName>' -ForEach $files -AllowNullOrEmptyForEach -Tag 'FunctionQA' { Context 'Code Quality Check' { It 'is valid PowerShell Code' { $psFile = Get-Content -Path $_ -ErrorAction Stop $errors = $null $null = [System.Management.Automation.PSParser]::Tokenize($psFile, [ref]$errors) $errors.Count | Should-Be 0 } It 'passes ScriptAnalyzer' { $saResults = Invoke-ScriptAnalyzer -Path $_ -Settings $ScriptAnalyzerSettings @($saResults).Count | Should-Be 0 -Because $($saResults.Message -join ';') } It 'has comment-based help .SYNOPSIS' { $content = Get-Content -Path $_.FullName -Raw $ast = [System.Management.Automation.Language.Parser]::ParseInput($content, [ref]$null, [ref]$null) $functionDefs = $ast.FindAll({ $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] }, $false) $functionHelp = $functionDefs.GetHelpContent() $functionHelp.Synopsis | Should-NotBeWhiteSpaceString } It 'has comment-based help .DESCRIPTION' { $content = Get-Content -Path $_.FullName -Raw $ast = [System.Management.Automation.Language.Parser]::ParseInput($content, [ref]$null, [ref]$null) $functionDefs = $ast.FindAll({ $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] }, $false) $functionHelp = $functionDefs.GetHelpContent() $functionHelp.Description | Should-NotBeWhiteSpaceString } It 'has comment-based help with at least one .EXAMPLE' { $content = Get-Content -Path $_.FullName -Raw $ast = [System.Management.Automation.Language.Parser]::ParseInput($content, [ref]$null, [ref]$null) $functionDefs = $ast.FindAll({ $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] }, $false) $functionHelp = $functionDefs.GetHelpContent() $functionHelp.Examples.Count | Should-BeGreaterThan 0 $functionHelp.Examples[0] | Should-MatchString ([regex]::Escape($_.BaseName)) } It 'has comment-based help examples with a blank line between code and description' { $content = Get-Content -Path $_.FullName -Raw $ast = [System.Management.Automation.Language.Parser]::ParseInput($content, [ref]$null, [ref]$null) $functionDefs = $ast.FindAll({ $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] }, $false) $functionHelp = $functionDefs.GetHelpContent() foreach ($example in $functionHelp.Examples) { $exampleSections = @( $example -split '\r?\n\s*\r?\n' | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' } ) $exampleSections.Count | Should-BeGreaterThanOrEqual 2 -Because ('the example in {0} must contain code, then a blank line, then a description paragraph' -f $_.BaseName) $descriptionLines = @( $exampleSections[1] -split '\r?\n' | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' } ) $descriptionLines.Count | Should-BeGreaterThan 0 -Because ('the example description in {0} must not be empty' -f $_.BaseName) } } It 'has comment-based help with .PARAMETER for each declared parameter' { $content = Get-Content -Path $_.FullName -Raw $ast = [System.Management.Automation.Language.Parser]::ParseInput($content, [ref]$null, [ref]$null) $functionDefs = $ast.FindAll({ $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] }, $false) $functionHelp = $functionDefs.GetHelpContent() $parameters = $functionDefs.Body.ParamBlock.Parameters.Name.VariablePath.ForEach({ $_.ToString() }) foreach ($parameter in $parameters) { $functionHelp.Parameters.($parameter.ToUpper()) | Should-NotBeWhiteSpaceString -Because ('the parameter {0} must have a .PARAMETER definition' -f $parameter) } } } } Describe 'Function and File Name Consistency' -Tag 'FunctionQA' { Context 'Public Function and File Naming Consistency' { It 'public functions should have matching file and function names' { if ($script:publicFunctions.Count -eq 0) { Set-ItResult -Skip -Because 'No public functions found' return } $results = @() foreach ($file in $script:publicFunctions) { try { $content = Get-Content -Path $file.FullName -Raw $ast = [System.Management.Automation.Language.Parser]::ParseInput($content, [ref]$null, [ref]$null) $functionDefs = $ast.FindAll({ $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] }, $false) if ($functionDefs.Count -eq 0) { $results += "File '$($file.Name)' - no function declaration found" } else { $functionName = $functionDefs[0].Name $fileName = $file.BaseName if ($functionName -ne $fileName) { $results += "File '$($file.Name)' contains function '$functionName' (expected '$fileName')" } } } catch { $results += "File '$($file.Name)' - failed to parse: $($_.Exception.Message)" } } $results.Count | Should-Be 0 -Because ($results -join '; ') } } Context 'Private Function and File Naming Consistency' { It 'private functions should have matching file and function names' { if ($script:privateFunctions.Count -eq 0) { Set-ItResult -Skip -Because 'No private functions found' return } $results = @() foreach ($file in $script:privateFunctions) { try { $content = Get-Content -Path $file.FullName -Raw $ast = [System.Management.Automation.Language.Parser]::ParseInput($content, [ref]$null, [ref]$null) $functionDefs = $ast.FindAll({ $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] }, $false) if ($functionDefs.Count -eq 0) { $results += "File '$($file.Name)' - no function declaration found" } else { $functionName = $functionDefs[0].Name $fileName = $file.BaseName if ($functionName -ne $fileName) { $results += "File '$($file.Name)' contains function '$functionName' (expected '$fileName')" } } } catch { $results += "File '$($file.Name)' - failed to parse: $($_.Exception.Message)" } } $results.Count | Should-Be 0 -Because ($results -join '; ') } } } Describe 'Built Module Testing' -Tag 'ModuleQA' { Context 'Module files exist' { It "$($data.ProjectName).psm1 should exist" { if (-not $script:psmPresent) { Set-ItResult -Skip return } $script:psmPresent | Should-BeTrue } It "$($data.ProjectName).psd1 should exist" { if (-not $script:psmPresent) { Set-ItResult -Skip return } $script:psdPresent | Should-BeTrue } } Context 'PowerShell Script Module (psm1) file' { It 'is valid PowerShell Code' { if (-not $script:psmPresent) { Set-ItResult -Skip return } $psFile = Get-Content -Path $data.ModuleFilePSM1 -ErrorAction Stop $errors = $null $null = [System.Management.Automation.PSParser]::Tokenize($psFile, [ref]$errors) $errors.Count | Should-Be 0 } It 'passes ScriptAnalyzer' { if (-not $script:psmPresent) { Set-ItResult -Skip return } $saResults = Invoke-ScriptAnalyzer -Path $data.ModuleFilePSM1 -Settings $ScriptAnalyzerSettings @($saResults).Count | Should-Be 0 -Because $($saResults.Message -join ';') } } Context 'Manifest (psd1) file' { BeforeAll { if (Test-Path -Path $data.ManifestFilePSD1) { $script:manifest = Import-PowerShellDataFile -Path $data.ManifestFilePSD1 } } It 'passes Test-ModuleManifest validation' { if (-not $script:psdPresent) { Set-ItResult -Skip return } $caughtError = $null try { Test-ModuleManifest -Path $data.ManifestFilePSD1 -ErrorAction Stop | Out-Null } catch { $caughtError = $_ } $caughtError | Should-BeNull } It 'is RootModule correct' { if (-not $script:psdPresent) { Set-ItResult -Skip return } "$($data.ProjectName).psm1" | Should-Be $script:manifest.RootModule } It 'should have ModuleVersion matching moduleproject.json' { if (-not $script:psdPresent) { Set-ItResult -Skip return } [version]$sv = [semver]$data.Version $sv | Should-Be $script:manifest.ModuleVersion } It 'should have Prerelease matching moduleproject.json' { if (-not $script:psdPresent) { Set-ItResult -Skip return } $sv = [semver]$data.Version $sv.PreReleaseLabel | Should-Be $script:manifest.PrivateData.PSData.Prerelease } It 'should have GUID matching moduleproject.json' { if (-not $script:psdPresent) { Set-ItResult -Skip return } $data.Manifest.GUID | Should-Be $script:manifest.GUID } It 'should have Author matching moduleproject.json' { if (-not $script:psdPresent) { Set-ItResult -Skip return } $data.Manifest.Author | Should-Be $script:manifest.Author } It 'should have CompanyName matching moduleproject.json' { if (-not $script:psdPresent) { Set-ItResult -Skip return } if ([string]::IsNullOrEmpty($data.Manifest.CompanyName)) { $company = 'Unknown' } else { $company = $data.Manifest.CompanyName } $company | Should-Be $script:manifest.CompanyName } It 'should have Copyright matching moduleproject.json' { if (-not $script:psdPresent) { Set-ItResult -Skip return } if ([string]::IsNullOrEmpty($data.Manifest.CompanyName)) { $copyright = "(c) $($data.Manifest.Author). All rights reserved." } else { $copyright = "(c) $($data.Manifest.CompanyName). All rights reserved." } ($copyright -eq $script:manifest.Copyright) | Should-BeTrue } It 'should have PowerShellVersion matching moduleproject.json' { if (-not $script:psdPresent) { Set-ItResult -Skip return } $data.Manifest.PowerShellVersion | Should-Be $script:manifest.PowerShellVersion } It 'should have RequiredModules matching moduleproject.json' { if (-not $script:psdPresent) { Set-ItResult -Skip return } $manifestModules = @($script:manifest.RequiredModules) $projectModules = @($data.Manifest.RequiredModules) $manifestModules.Count | Should-Be $projectModules.Count -Because 'manifest should have same number of required modules as moduleassember.json' if ($projectModules.Count -gt 0) { foreach ($i in 0..($projectModules.Count - 1)) { $projectModule = $projectModules[$i] $manifestModule = $manifestModules[$i] $manifestName = if ($manifestModule -is [string]) { $manifestModule } else { $manifestModule.ModuleName } $manifestName | Should-Be $projectModule.ModuleName if ($projectModule.ModuleVersion) { $manifestModule.ModuleVersion | Should-Be $projectModule.ModuleVersion } if ($projectModule.MaximumVersion) { $manifestModule.MaximumVersion | Should-Be $projectModule.MaximumVersion } if ($projectModule.RequiredVersion) { $manifestModule.RequiredVersion | Should-Be $projectModule.RequiredVersion } if ($projectModule.GUID) { $manifestModule.GUID | Should-Be $projectModule.GUID } } } } It 'should have FunctionsToExport matching the public function names' { if (-not $script:psdPresent) { Set-ItResult -Skip return } $expectedFunctions = $script:publicFunctions.BaseName | Sort-Object $exportedFunctions = @($script:manifest.FunctionsToExport) | Sort-Object $exportedFunctions | Should-NotContainCollection '*' -Because 'wildcard FunctionsToExport harms module load performance' $exportedFunctions.Count | Should-Be $expectedFunctions.Count -Because 'manifest FunctionsToExport count should match public function count' if ($expectedFunctions.Count -gt 0) { Should-BeCollection -Actual $exportedFunctions -Expected $expectedFunctions } } It 'should have AliasesToExport matching public function aliases' { if (-not $script:psdPresent) { Set-ItResult -Skip return } $expectedAliases = @( foreach ($file in $script:publicFunctions) { $ast = [System.Management.Automation.Language.Parser]::ParseFile($file.FullName, [ref]$null, [ref]$null) $functionNode = $ast.FindAll({ $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] }, $true)[0] if ($functionNode) { ($functionNode.Body.ParamBlock.Attributes | Where-Object { $_.TypeName -like 'Alias' } | ForEach-Object PositionalArguments).Value } } ) | Sort-Object $rawAliasesToExport = @($script:manifest.AliasesToExport) | Sort-Object $rawAliasesToExport | Should-NotContainCollection '*' -Because 'wildcard AliasesToExport harms module load performance' $rawAliasesToExport.Count | Should-Be $expectedAliases.Count -Because 'manifest AliasesToExport count should match aliases defined in public functions' if ($expectedAliases.Count -gt 0) { Should-BeCollection -Actual $rawAliasesToExport -Expected $expectedAliases } } } } Describe 'General Module Control' -Tag 'ModuleQA' { It 'should import without error' { if (-not $script:psmPresent -or -not $script:psdPresent) { Set-ItResult -Skip return } $caughtError = $null try { Import-Module -Name $data.OutputModuleDir -Force -ErrorAction Stop } catch { $caughtError = $_ } $caughtError | Should-BeNull Get-Module -Name $data.ProjectName | Should-NotBeNull } It 'should remove without error' { if (-not $script:psmPresent -or -not $script:psdPresent) { Set-ItResult -Skip return } if (-not (Get-Module -Name $data.ProjectName)) { Import-Module -Name $data.OutputModuleDir -Force -ErrorAction Stop } $caughtError = $null try { Remove-Module -Name $data.ProjectName -ErrorAction Stop } catch { $caughtError = $_ } $caughtError | Should-BeNull Get-Module $data.ProjectName | Should-BeNull } } |