Scripts/ModuleSchemaTests.ps1
<#
.SYNOPSIS The module schema test definition. .DESCRIPTION Invoke tests based on Pester to verify if the module is valid. This includes the meta files for VS Code, built system, git repository but also module specific files. #> [CmdletBinding()] param ( # Root path of the project. [Parameter(Mandatory = $true)] [System.String] $BuildRoot, # Name of the module. [Parameter(Mandatory = $true)] [System.String] $ModuleName, # List of text file extension. [Parameter(Mandatory = $true)] [System.String[]] $TextFileExtension ) Describe 'Module Schema' { Context 'File Structure' { $fileNames = @( @{ RelativePath = '.vscode\launch.json' } @{ RelativePath = '.vscode\settings.json' } @{ RelativePath = '.vscode\tasks.json' } @{ RelativePath = "$ModuleName\en-US\about_$ModuleName.help.txt" } @{ RelativePath = "$ModuleName\Resources\$ModuleName.Formats.ps1xml" } @{ RelativePath = "$ModuleName\Resources\$ModuleName.Types.ps1xml" } @{ RelativePath = "$ModuleName\$ModuleName.psd1" } @{ RelativePath = "$ModuleName\$ModuleName.psm1" } @{ RelativePath = '.build.ps1' } @{ RelativePath = '.debug.ps1' } @{ RelativePath = '.gitignore' } @{ RelativePath = 'CHANGELOG.md' } @{ RelativePath = 'LICENSE' } @{ RelativePath = 'README.md' } ) It 'Should have the file <RelativePath>' -TestCases $fileNames { param ($RelativePath) # Arrange $path = Join-Path -Path $BuildRoot -ChildPath $RelativePath # Act $actual = Test-Path -Path $path # Assert $actual | Should -BeTrue } } Context 'File Encoding & Formatting' { $fileNames = Get-ChildItem -Path $BuildRoot -File -Recurse | Where-Object { $TextFileExtension -contains $_.Extension -and $_.FullName -notlike "$BuildRoot*\out\*" -and $_.FullName -notlike "$BuildRoot*\bin\*" -and $_.FullName -notlike "$BuildRoot*\obj\*" -and $_.FullName -notlike "$BuildRoot\*\packages\*" } | ForEach-Object { @{ Path = $_.FullName; RelativePath = $_.FullName.Replace($BuildRoot, '') } } It 'Should not use UTF-16 LE encoding for file <RelativePath>' -TestCases $fileNames { param ($Path) # Act $zeroByteCount = @([System.IO.File]::ReadAllBytes($Path) -eq 0).Length # Assert $zeroByteCount | Should -Be 0 -Because 'the text file should not contain 0x00 bytes' } It 'Should not use BOM for UTF-8 encoding for file <RelativePath>' -TestCases $fileNames { param ($Path) # Act $bytes = [System.IO.File]::ReadAllBytes($Path) $isBOM = $bytes.Length -ge 3 -and $bytes[0] -eq 239 -and $bytes[1] -eq 187 -and $bytes[2] -eq 191 # Assert $isBOM | Should -BeFalse -Because 'the text file should not contain the UTF-8 BOM header' } It 'Should use spaces for indentation (not tabs) for file <RelativePath>' -TestCases $fileNames { param ($Path) # Arrange $failedLines = @() # Act $content = Get-Content -Path $Path for ($i = 0; $i -lt $content.Count; $i++) { if ($content[$i] -match '^\s*\t') { $failedLines += ($i + 1) } } # Assert $failedLines.Count | Should -Be 0 -Because "the line(s) $($failedLines -join ', ') should not contain a tab indentation" } It 'Should use no trailing spaces on lines for file <RelativePath>' -TestCases $fileNames.Where({ $_.RelativePath -ne '\README.md' }) { param ($Path) # Arrange $failedLines = @() # Act $content = Get-Content -Path $Path for ($i = 0; $i -lt $content.Count; $i++) { if ($content[$i] -match '\s+$') { $failedLines += ($i + 1) } } # Assert $failedLines.Count | Should -Be 0 -Because "the line(s) $($failedLines -join ', ') should not contain trailing spaces" } } Context 'VS Code Configuration' { It 'Should have a valid .vscode\launch.json' { # Arrange $path = Join-Path -Path $BuildRoot -ChildPath '.vscode\launch.json' # Act $launch = Get-Content -Path $path | ConvertFrom-Json # Assert $launch.'version' | Should -Be '0.2.0' $launch.'configurations'[0].'name' | Should -Be 'PowerShell Interactive' $launch.'configurations'[0].'type' | Should -Be 'PowerShell' $launch.'configurations'[0].'request' | Should -Be 'launch' $launch.'configurations'[0].'cwd' | Should -Be '${workspaceFolder}' $launch.'configurations'[0].'createTemporaryIntegratedConsole' | Should -BeTrue $launch.'configurations'[1].'name' | Should -Be 'PowerShell Debug Script' $launch.'configurations'[1].'type' | Should -Be 'PowerShell' $launch.'configurations'[1].'request' | Should -Be 'launch' $launch.'configurations'[1].'script' | Should -Be '${workspaceFolder}\.debug.ps1' $launch.'configurations'[1].'cwd' | Should -Be '${workspaceFolder}' $launch.'configurations'[1].'createTemporaryIntegratedConsole' | Should -BeTrue } It 'Should have a valid .vscode\settings.json' { # Arrange $path = Join-Path -Path $BuildRoot -ChildPath '.vscode\settings.json' # Act $settings = Get-Content -Path $path | ConvertFrom-Json # Assert $settings.'files.trimTrailingWhitespace' | Should -BeTrue $settings.'[markdown]'.'files.trimTrailingWhitespace' | Should -BeFals $settings.'files.exclude'.'**/.git' | Should -BeTrue $settings.'files.exclude'.'**/.vs' | Should -BeTrue $settings.'files.exclude'.'**/obj' | Should -BeTrue $settings.'files.exclude'.'**/bin' | Should -BeTrue $settings.'files.exclude'.'**/out' | Should -BeTrue $settings.'files.exclude'.'**/packages' | Should -BeTrue $settings.'files.exclude'.'**/*.user' | Should -BeTrue $settings.'search.exclude'.'**/.git' | Should -BeTrue $settings.'search.exclude'.'**/.vs' | Should -BeTrue $settings.'search.exclude'.'**/obj' | Should -BeTrue $settings.'search.exclude'.'**/bin' | Should -BeTrue $settings.'search.exclude'.'**/out' | Should -BeTrue $settings.'search.exclude'.'**/packages' | Should -BeTrue $settings.'search.exclude'.'**/*.user' | Should -BeTrue $settings.'powershell.debugging.createTemporaryIntegratedConsole' | Should -BeTrue $settings.'powershell.codeFormatting.alignPropertyValuePairs' | Should -BeTrue $settings.'powershell.codeFormatting.ignoreOneLineBlock' | Should -BeTrue $settings.'powershell.codeFormatting.newLineAfterCloseBrace' | Should -BeTrue $settings.'powershell.codeFormatting.newLineAfterOpenBrace' | Should -BeTrue $settings.'powershell.codeFormatting.openBraceOnSameLine' | Should -BeFalse $settings.'powershell.codeFormatting.pipelineIndentationStyle' | Should -Be 'IncreaseIndentationAfterEveryPipeline' $settings.'powershell.codeFormatting.useCorrectCasing' | Should -BeTrue $settings.'powershell.codeFormatting.whitespaceAfterSeparator' | Should -BeTrue $settings.'powershell.codeFormatting.whitespaceAroundOperator' | Should -BeTrue $settings.'powershell.codeFormatting.WhitespaceAroundPipe' | Should -BeTrue $settings.'powershell.codeFormatting.whitespaceBeforeOpenBrace' | Should -BeTrue $settings.'powershell.codeFormatting.whitespaceBeforeOpenParen' | Should -BeTrue $settings.'powershell.codeFormatting.WhitespaceInsideBrace' | Should -BeTrue } It 'Should have a valid .vscode\tasks.json' { # Arrange $path = Join-Path -Path $BuildRoot -ChildPath '.vscode\tasks.json' # Act $tasks = Get-Content -Path $path | ConvertFrom-Json # Assert $tasks.version | Should -Be '2.0.0' $tasks.command | Should -Be '"& { Invoke-Build -Task $args }"' $tasks.type | Should -Be 'shell' $tasks.options.shell.executable | Should -Be 'powershell.exe' $tasks.options.shell.args[0] | Should -Be '-NoProfile' $tasks.options.shell.args[1] | Should -Be '-Command' $tasks.presentation.echo | Should -BeFalse $tasks.presentation.reveal | Should -Be 'always' $tasks.presentation.focus | Should -BeFalse $tasks.presentation.panel | Should -Be 'new' $tasks.tasks[0].label | Should -Be 'Test' $tasks.tasks[0].group.kind | Should -Be 'test' $tasks.tasks[0].group.isDefault | Should -BeTrue $tasks.tasks[1].label | Should -Be 'Build' $tasks.tasks[1].group.kind | Should -Be 'build' $tasks.tasks[1].group.isDefault | Should -BeTrue } } Context 'Git Configuration' { It 'Should have a valid .gitignore' { # Arrange $path = Join-Path -Path $BuildRoot -ChildPath '.gitignore' # Act $gitignore = Get-Content -Path $path # Assert $gitignore | Should -Contain '**/Assemblies/*.dll' $gitignore | Should -Contain '[Bb]in/' $gitignore | Should -Contain '[Oo]bj/' $gitignore | Should -Contain '[Oo]ut/' $gitignore | Should -Contain '.vs/' $gitignore | Should -Contain '**/packages/*' } } Context 'Module Loader' { It 'Should load the module without any errors' { # Arrange $path = '{0}\{1}\{1}.psd1' -f $BuildRoot, $ModuleName # Act & Assert { Import-Module -Name $path -Force -ErrorAction Stop } | Should -Not -Throw } } Context 'Module Definition' { # Get the name of all helper files $helperFiles = Get-ChildItem -Path "$BuildRoot\$ModuleName" -Filter 'Helpers' | Get-ChildItem -Include '*.ps1' -File -Recurse | Sort-Object -Property 'BaseName' | ForEach-Object { @{ Name = $_.BaseName } } # Get the name of all function files $functionFiles = Get-ChildItem -Path "$BuildRoot\$ModuleName" -Filter 'Functions' | Get-ChildItem -Include '*.ps1' -File -Recurse | Sort-Object -Property 'BaseName' | ForEach-Object { @{ Name = $_.BaseName } } # Get the list of all exported functions $functionExportNames = Import-PowerShellDataFile -Path "$BuildRoot\$ModuleName\$ModuleName.psd1" | ForEach-Object { $_['FunctionsToExport'] } | Sort-Object | ForEach-Object { @{ Name = $_ } } It 'Should not export helper functions <Name>' -TestCases $helperFiles -Skip:($helperFiles.Count -eq 0) { param ($Name) # Act $actual = Import-PowerShellDataFile -Path "$BuildRoot\$ModuleName\$ModuleName.psd1" | ForEach-Object { $_['FunctionsToExport'] } # Assert $actual | Should -Not -Contain $Name } It 'Should export function <Name>' -TestCases $functionFiles -Skip:($functionFiles.Count -eq 0) { param ($Name) # Act $actual = Import-PowerShellDataFile -Path "$BuildRoot\$ModuleName\$ModuleName.psd1" | ForEach-Object { $_['FunctionsToExport'] } # Assert $actual | Should -Contain $Name } It 'Should have the script file for the function <Name>' -TestCases $functionExportNames -Skip:($functionExportNames.Count -eq 0) { param ($Name) # Act $actual = Get-ChildItem -Path "$BuildRoot\$ModuleName" -Filter 'Functions' | Get-ChildItem -Filter "$Name.ps1" -File -Recurse # Assert $actual.Count | Should -Be 1 } } Context 'Module Function' { $scriptFiles = @() $scriptFiles += Get-ChildItem -Path "$BuildRoot\$ModuleName" -Filter 'Helpers' | Get-ChildItem -Include '*.ps1' -File -Recurse $scriptFiles += Get-ChildItem -Path "$BuildRoot\$ModuleName" -Filter 'Functions' | Get-ChildItem -Include '*.ps1' -File -Recurse foreach ($scriptFile in $scriptFiles) { Context $scriptFile.FullName.Replace("$BuildRoot\", 'File ') { # } } # Use AST / Parser # [System.Management.Automation.Language.Parser]::ParseInput($MyInvocation.MyCommand.ScriptContents, [ref]$null, [ref]$null) # Ensure function matches filename # Ensure function help: # - SYNOPSIS, DESCRIPTION, EXAMPLE # - INPUT (if defined) # - OUTPUT (if deinfed) # - NOTES (Author, Repo, License) # - LINK (to the repository, combine with git remote) # - Parameter Help } } |