tests/PublicSurface.Tests.ps1
|
#Requires -Version 7.0 Describe 'Public surface consistency' { BeforeAll { $script:repo = Split-Path $PSScriptRoot -Parent # The core and its on-demand modules: each manifest exports its own Public/ folder. $script:psd1Paths = @( (Join-Path $script:repo 'DotForge.psd1'), (Join-Path $script:repo 'Modules' 'DotForge.Catalog' 'DotForge.Catalog.psd1'), (Join-Path $script:repo 'Modules' 'DotForge.Helpers' 'DotForge.Helpers.psd1') ) $script:psd1Path = $script:psd1Paths[0] $script:buildScriptPath = Join-Path $script:repo 'build' 'Build-DFReferenceDocs.ps1' # Normalizes manifest comment headers to reference doc sections function script:ConvertTo-RefSection([string]$ManifestComment) { switch -Wildcard ($ManifestComment) { 'Layer 1*' { 'Core' } 'Layer 2*' { 'Core' } 'Layer 3*' { 'Core' } 'General Helpers — Help & Discovery' { 'Help and discovery' } 'General Helpers — Navigation' { 'Navigation' } 'General Helpers — File System' { 'Files' } 'General Helpers — Process' { 'Processes' } 'General Helpers — Environment & Profile' { 'Environment and profile' } 'General Helpers — Clipboard' { 'Clipboard, pager and utilities' } 'General Helpers — Utility' { 'Clipboard, pager and utilities' } 'Catalog Info (trifle)' { 'Package catalog' } default { throw "Unknown manifest group comment: '$ManifestComment'" } } } # The three manifests' exports, as one surface. $script:manifestData = @{ FunctionsToExport = @(foreach ($p in $script:psd1Paths) { (Import-PowerShellDataFile $p).FunctionsToExport }) AliasesToExport = (Import-PowerShellDataFile $script:psd1Path).AliasesToExport } # Discover functions defined at top level in each module's Public/*.ps1 via PowerShell AST $script:publicFiles = @(@(foreach ($p in $script:psd1Paths) { Get-ChildItem -Path (Join-Path (Split-Path $p) 'Public') -Filter '*.ps1' }) | Sort-Object Name) $script:publicFunctionMap = [ordered]@{} # FunctionName -> FileBaseName $script:topLevelFunctions = [System.Collections.Generic.List[string]]::new() foreach ($file in $script:publicFiles) { $fTokens = $null $fErrors = $null $fileAst = [System.Management.Automation.Language.Parser]::ParseFile( $file.FullName, [ref]$fTokens, [ref]$fErrors ) $fnAsts = $fileAst.FindAll({ param($n) $n -is [System.Management.Automation.Language.FunctionDefinitionAst] -and $n.Parent -eq $fileAst.EndBlock }, $false) foreach ($fn in $fnAsts) { $script:topLevelFunctions.Add($fn.Name) $script:publicFunctionMap[$fn.Name] = $file.BaseName } } # Extract FunctionsToExport comment groups from each manifest's AST & tokens $script:manifestFunctionGroups = [ordered]@{} foreach ($p in $script:psd1Paths) { $tokens = $null $errors = $null $manifestAst = [System.Management.Automation.Language.Parser]::ParseFile($p, [ref]$tokens, [ref]$errors) $manifestHash = $manifestAst.FindAll({ param($n) $n -is [System.Management.Automation.Language.HashtableAst] }, $true) | Select-Object -First 1 $fteEntry = foreach ($kv in $manifestHash.KeyValuePairs) { if ($kv.Item1.Extent.Text -eq 'FunctionsToExport') { $kv; break } } $fteTokens = $tokens | Where-Object { $_.Extent.StartOffset -ge $fteEntry.Item2.Extent.StartOffset -and $_.Extent.EndOffset -le $fteEntry.Item2.Extent.EndOffset } $currentGroup = $null foreach ($t in $fteTokens) { if ($t.Kind -eq [System.Management.Automation.Language.TokenKind]::Comment) { $currentGroup = $t.Text.TrimStart('#').Trim() } elseif ($t.Kind -eq [System.Management.Automation.Language.TokenKind]::StringLiteral) { $script:manifestFunctionGroups[$t.Value] = $currentGroup } } } # Parse sectionByFile from build/Build-DFReferenceDocs.ps1 via AST $bTokens = $null $bErrors = $null $buildAst = [System.Management.Automation.Language.Parser]::ParseFile( $script:buildScriptPath, [ref]$bTokens, [ref]$bErrors ) $assign = $buildAst.FindAll({ param($n) $n -is [System.Management.Automation.Language.AssignmentStatementAst] -and $n.Left.Extent.Text -like '*sectionByFile*' }, $true) | Select-Object -First 1 $hashAst = $assign.Right.FindAll({ param($n) $n -is [System.Management.Automation.Language.HashtableAst] }, $true) | Select-Object -First 1 $script:sectionByFile = [ordered]@{} foreach ($kv in $hashAst.KeyValuePairs) { $key = $kv.Item1.Extent.Text.Trim("'`"") $val = $kv.Item2.Extent.Text.Trim("'`"") $script:sectionByFile[$key] = $val } } Context 'FunctionsToExport parity with Public/*.ps1 (1a)' { It 'lists every function defined at top level in Public/*.ps1 in FunctionsToExport' { $missingFromManifest = @($script:topLevelFunctions | Where-Object { $_ -notin $script:manifestData.FunctionsToExport }) $missingFromManifest | Should -BeNullOrEmpty -Because 'every top-level function defined in Public/*.ps1 must be listed in DotForge.psd1 FunctionsToExport' } It 'defines every FunctionsToExport entry at top level in Public/*.ps1' { $missingFromPublic = @($script:manifestData.FunctionsToExport | Where-Object { $_ -notin $script:topLevelFunctions }) $missingFromPublic | Should -BeNullOrEmpty -Because 'every function listed in DotForge.psd1 FunctionsToExport must be defined at top level in Public/*.ps1' } } Context 'AliasesToExport resolution (1b)' { BeforeAll { Import-Module (Join-Path $script:repo 'DotForge.psd1') -Force } AfterAll { Remove-Module DotForge -ErrorAction SilentlyContinue } It 'resolves every alias in AliasesToExport after Import-Module' { $unresolved = foreach ($alias in $script:manifestData.AliasesToExport) { $cmd = Get-Command -Name $alias -ErrorAction SilentlyContinue if (-not $cmd) { $alias } } $unresolved | Should -BeNullOrEmpty -Because 'every alias in AliasesToExport must resolve to a command after importing DotForge' } } Context 'Reference doc section grouping consistency (1c)' { It 'assigns each public function to the same section in the manifest and Build-DFReferenceDocs.ps1' { $mismatches = [System.Collections.Generic.List[string]]::new() foreach ($fn in $script:manifestData.FunctionsToExport) { $file = $script:publicFunctionMap[$fn] $docSection = $script:sectionByFile[$file] $manifestComment = $script:manifestFunctionGroups[$fn] $expectedSection = script:ConvertTo-RefSection $manifestComment if ($docSection -ne $expectedSection) { $mismatches.Add("Function '$fn' in '$file': docSection='$docSection' vs expected='$expectedSection' (from '$manifestComment')") } } $mismatches | Should -BeNullOrEmpty -Because 'each public function must land in the same group in both the manifest and Build-DFReferenceDocs.ps1' } } Context 'Build-DFReferenceDocs sectionByFile coverage (1d)' { It 'explicitly maps every Public/*.ps1 file in sectionByFile without silent fallback' { # Files that define public functions (DFAliases.ps1 only defines aliases). $unmapped = foreach ($file in $script:publicFiles | Where-Object { $_.BaseName -in $script:publicFunctionMap.Values }) { if (-not $script:sectionByFile.Contains($file.BaseName)) { $file.Name } } $unmapped | Should -BeNullOrEmpty -Because 'every Public/*.ps1 file must be explicitly present in sectionByFile with no silent fallback' } } } |