tests/Docs.Help.Tests.ps1
|
#Requires -Version 7.0 # Every user-facing function must carry complete comment-based help: the # module's exported functions, and the global functions each Tools/*.ps1 # companion defines (fco, wins, glow, ...). Sidecars are parsed, never run, so # this needs none of the tools installed. BeforeDiscovery { $repo = Split-Path $PSScriptRoot -Parent Import-Module (Join-Path $repo 'DotForge.psd1') -Force 3>$null $script:exported = @(Get-Command -Module DotForge -CommandType Function | Sort-Object Name | ForEach-Object { @{ Name = $_.Name } }) # Global functions a sidecar defines, with the scriptblock that holds their help. $script:sidecarFns = foreach ($file in Get-ChildItem (Join-Path $repo 'Tools') -Filter '*.ps1') { $ast = [System.Management.Automation.Language.Parser]::ParseFile($file.FullName, [ref]$null, [ref]$null) foreach ($fn in $ast.FindAll({ param($n) $n -is [System.Management.Automation.Language.FunctionDefinitionAst] }, $true)) { if ($fn.Name -like 'global:*') { @{ File = $file.Name; Name = $fn.Name -replace '^global:'; Body = $fn.Body.Extent.Text } } } foreach ($cmd in $ast.FindAll({ param($n) $n -is [System.Management.Automation.Language.CommandAst] }, $true)) { if ($cmd.GetCommandName() -ne 'Set-Item') { continue } $path = $cmd.CommandElements | Where-Object { $_ -is [System.Management.Automation.Language.StringConstantExpressionAst] -and $_.Value -like 'function:global:*' } | Select-Object -First 1 $sb = $cmd.FindAll({ param($n) $n -is [System.Management.Automation.Language.ScriptBlockExpressionAst] }, $true) | Select-Object -First 1 if ($path -and $sb) { @{ File = $file.Name; Name = $path.Value -replace '^function:global:'; Body = $sb.ScriptBlock.Extent.Text } } } } } BeforeAll { # Returns the problems with one help object; empty when the help is complete. function Get-HelpGap { param($Help, [string[]]$ParameterNames) if (-not $Help) { return 'no comment-based help' } if (-not $Help.Synopsis) { 'SYNOPSIS' } if (-not $Help.Description) { 'DESCRIPTION' } if (-not $Help.Examples) { 'EXAMPLE' } if (-not $Help.Outputs) { 'OUTPUTS' } foreach ($p in $ParameterNames) { if (-not $Help.Parameters -or -not $Help.Parameters[$p.ToUpperInvariant()]) { "PARAMETER $p" } } } # Parameters a function declares, minus the common ones CmdletBinding adds. function Get-DeclaredParameter { param([System.Management.Automation.Language.ScriptBlockAst]$Ast) $params = $Ast.ParamBlock?.Parameters @($params | ForEach-Object { $_.Name.VariablePath.UserPath }) } $script:common = [System.Management.Automation.PSCmdlet]::CommonParameters + [System.Management.Automation.PSCmdlet]::OptionalCommonParameters } Describe 'Exported function help' { It '<Name> has complete comment-based help' -ForEach $script:exported { $cmd = Get-Command $Name $help = $cmd.ScriptBlock.Ast.Body.GetHelpContent() $params = @($cmd.Parameters.Keys | Where-Object { $_ -notin $script:common }) @(Get-HelpGap -Help $help -ParameterNames $params) | Should -BeNullOrEmpty } It '<Name> help examples parse' -ForEach $script:exported { $help = (Get-Command $Name).ScriptBlock.Ast.Body.GetHelpContent() foreach ($example in @($help.Examples)) { # Example text: the code lines come first, then a blank line, then prose. $code = ($example -split '\r?\n\s*\r?\n', 2)[0] $errors = $null $null = [System.Management.Automation.Language.Parser]::ParseInput($code, [ref]$null, [ref]$errors) $errors | Should -BeNullOrEmpty -Because "example in $Name`:`n$code" } } } Describe 'Sidecar global function help' { It '<Name> (<File>) has complete comment-based help' -ForEach $script:sidecarFns { $ast = [System.Management.Automation.Language.Parser]::ParseInput($Body, [ref]$null, [ref]$null) # The body is a '{ ... }' scriptblock; its single statement holds the function body. $inner = $ast.Find({ param($n) $n -is [System.Management.Automation.Language.ScriptBlockAst] -and $n -ne $ast }, $true) $target = if ($inner) { $inner } else { $ast } $help = $target.GetHelpContent() @(Get-HelpGap -Help $help -ParameterNames (Get-DeclaredParameter $target)) | Should -BeNullOrEmpty } } |