tools/Build-PublicUnitTests.ps1
|
<#
.SYNOPSIS Generate Pester 3 unit test files for every public GitEasy command. .DESCRIPTION Writes one Tests\Unit\<CommandName>.Tests.ps1 file per public command. Each test file uses Pester 3 syntax (Should Be / Should Match / Should Not BeNullOrEmpty) and asserts the command's contract: it is exported, declares the expected parameters with the right shapes (Mandatory / SwitchParameter / ValidateSet), and ships full CBH (Synopsis + at least one Example). The generated files are deterministic — re-running this script regenerates them in place. Hand edits will be overwritten. .PARAMETER ProjectRoot Absolute path to the GitEasy source repository. Defaults to C:\Sysadmin\Scripts\GitEasy. .EXAMPLE .\tools\Build-PublicUnitTests.ps1 #> [CmdletBinding()] param( [string]$ProjectRoot = 'C:\Sysadmin\Scripts\GitEasy' ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' $publicRoot = Join-Path $ProjectRoot 'Public' $unitRoot = Join-Path $ProjectRoot 'Tests\Unit' if (-not (Test-Path -LiteralPath $publicRoot -PathType Container)) { throw "Missing Public folder: $publicRoot" } if (-not (Test-Path -LiteralPath $unitRoot -PathType Container)) { New-Item -Path $unitRoot -ItemType Directory -Force | Out-Null } Remove-Module GitEasy -Force -ErrorAction SilentlyContinue Import-Module (Join-Path $ProjectRoot 'GitEasy.psd1') -Force $publicFiles = @(Get-ChildItem -LiteralPath $publicRoot -Filter '*.ps1' -File | Sort-Object Name) $generated = 0 foreach ($file in $publicFiles) { $cmdName = $file.BaseName $cmd = Get-Command -Module GitEasy -Name $cmdName -ErrorAction SilentlyContinue if (-not $cmd) { Write-Warning "Skipping $cmdName - not exported by GitEasy." continue } # Inspect the AST to find the function's declared param block (drops the # auto-added common parameters that Get-Command surfaces). $tokens = $null; $errors = $null $ast = [System.Management.Automation.Language.Parser]::ParseFile($file.FullName, [ref]$tokens, [ref]$errors) $fnAst = @($ast.FindAll({ param($n) $n -is [System.Management.Automation.Language.FunctionDefinitionAst] }, $true)) | Select-Object -First 1 $declaredParams = @() if ($fnAst -and $fnAst.Body.ParamBlock -and $fnAst.Body.ParamBlock.Parameters) { foreach ($p in $fnAst.Body.ParamBlock.Parameters) { $name = $p.Name.VariablePath.UserPath $type = if ($p.StaticType) { $p.StaticType.Name } else { '' } $isMandatory = $false $validateSet = @() foreach ($attr in $p.Attributes) { if ($attr -is [System.Management.Automation.Language.AttributeAst]) { if ($attr.TypeName.Name -eq 'Parameter') { foreach ($na in $attr.NamedArguments) { if ($na.ArgumentName -eq 'Mandatory') { $isMandatory = $true } } } if ($attr.TypeName.Name -eq 'ValidateSet') { foreach ($pa in $attr.PositionalArguments) { if ($pa -is [System.Management.Automation.Language.StringConstantExpressionAst]) { $validateSet += $pa.Value } } } } } $declaredParams += [PSCustomObject]@{ Name = $name Type = $type Mandatory = $isMandatory IsSwitch = ($type -eq 'SwitchParameter') ValidateSet = $validateSet } } } $supportsShouldProcess = $false if ($fnAst -and $fnAst.Body.ParamBlock -and $fnAst.Body.ParamBlock.Attributes) { foreach ($attr in $fnAst.Body.ParamBlock.Attributes) { if ($attr -is [System.Management.Automation.Language.AttributeAst] -and $attr.TypeName.Name -eq 'CmdletBinding') { foreach ($na in $attr.NamedArguments) { if ($na.ArgumentName -eq 'SupportsShouldProcess') { $supportsShouldProcess = $true } } } } } $lines = New-Object System.Collections.Generic.List[string] $lines.Add('# Generated by tools\Build-PublicUnitTests.ps1. Re-run to regenerate.') $lines.Add('') $lines.Add('$ProjectRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)') $lines.Add('$ModulePath = Join-Path $ProjectRoot ''GitEasy.psd1''') $lines.Add('') $lines.Add("Describe '$cmdName (unit contract)' {") $lines.Add('') $lines.Add(' BeforeAll {') $lines.Add(' Remove-Module GitEasy -Force -ErrorAction SilentlyContinue') $lines.Add(' Import-Module $ModulePath -Force') $lines.Add(' }') $lines.Add('') $lines.Add(" It 'is exported by the GitEasy module' {") $lines.Add(" @(Get-Command -Module GitEasy -Name '$cmdName').Count | Should Be 1") $lines.Add(' }') $lines.Add('') $lines.Add(" It 'has CmdletBinding' {") $lines.Add(' $cmd = Get-Command ''' + $cmdName + '''') $lines.Add(' $cmd.CmdletBinding | Should Be $true') $lines.Add(' }') $lines.Add('') if ($supportsShouldProcess) { $lines.Add(" It 'supports ShouldProcess (-WhatIf and -Confirm available)' {") $lines.Add(' $cmd = Get-Command ''' + $cmdName + '''') $lines.Add(' $cmd.Parameters.ContainsKey(''WhatIf'') | Should Be $true') $lines.Add(' $cmd.Parameters.ContainsKey(''Confirm'') | Should Be $true') $lines.Add(' }') $lines.Add('') } foreach ($p in $declaredParams) { $lines.Add(" It 'declares the -$($p.Name) parameter' {") $lines.Add(' $cmd = Get-Command ''' + $cmdName + '''') $lines.Add(' $cmd.Parameters.ContainsKey(''' + $p.Name + ''') | Should Be $true') $lines.Add(' }') $lines.Add('') if ($p.IsSwitch) { $lines.Add(" It '-$($p.Name) is a switch parameter' {") $lines.Add(' $cmd = Get-Command ''' + $cmdName + '''') $lines.Add(' $cmd.Parameters[''' + $p.Name + '''].ParameterType.Name | Should Be ''SwitchParameter''') $lines.Add(' }') $lines.Add('') } if ($p.Mandatory) { $lines.Add(" It '-$($p.Name) is mandatory' {") $lines.Add(' $cmd = Get-Command ''' + $cmdName + '''') $lines.Add(' $attrs = $cmd.Parameters[''' + $p.Name + '''].Attributes') $lines.Add(' $paramAttr = $attrs | Where-Object { $_.GetType().Name -eq ''ParameterAttribute'' } | Select-Object -First 1') $lines.Add(' $paramAttr.Mandatory | Should Be $true') $lines.Add(' }') $lines.Add('') } if ($p.ValidateSet.Count -gt 0) { $lines.Add(" It '-$($p.Name) accepts only the documented values' {") $lines.Add(' $cmd = Get-Command ''' + $cmdName + '''') $lines.Add(' $set = ($cmd.Parameters[''' + $p.Name + '''].Attributes | Where-Object { $_.GetType().Name -eq ''ValidateSetAttribute'' } | Select-Object -First 1).ValidValues') foreach ($v in $p.ValidateSet) { $lines.Add(' $set -contains ''' + $v + ''' | Should Be $true') } $lines.Add(' }') $lines.Add('') } } $lines.Add(" It 'has a non-empty .SYNOPSIS' {") $lines.Add(' $help = Get-Help ''' + $cmdName + ''' -ErrorAction SilentlyContinue') $lines.Add(' $help.Synopsis | Should Not BeNullOrEmpty') $lines.Add(' }') $lines.Add('') $lines.Add(" It 'has at least one .EXAMPLE in CBH' {") $lines.Add(' $help = Get-Help ''' + $cmdName + ''' -Full -ErrorAction SilentlyContinue') $lines.Add(' @($help.examples.example).Count -gt 0 | Should Be $true') $lines.Add(' }') $lines.Add('}') $body = ($lines.ToArray() -join "`r`n") + "`r`n" $outPath = Join-Path $unitRoot ($cmdName + '.Tests.ps1') [System.IO.File]::WriteAllText($outPath, $body, [System.Text.UTF8Encoding]::new($false)) $generated++ } Write-Host '' Write-Host "Generated $generated public unit-test files in $unitRoot" -ForegroundColor Green |