From f592f3b4eafcf52e229ed8b3c6238a277097aa68 Mon Sep 17 00:00:00 2001 From: Gijs Reijn Date: Thu, 7 Nov 2024 13:20:36 +0100 Subject: [PATCH] Validating examples --- .../Microsoft.VSCode.Dsc/VSCodeExtension.md | 8 +- tests/QA/module.tests.ps1 | 268 +++++++++++++++++- 2 files changed, 271 insertions(+), 5 deletions(-) diff --git a/resources/Help/Microsoft.VSCode.Dsc/VSCodeExtension.md b/resources/Help/Microsoft.VSCode.Dsc/VSCodeExtension.md index 4f5e62b1..c00c47fc 100644 --- a/resources/Help/Microsoft.VSCode.Dsc/VSCodeExtension.md +++ b/resources/Help/Microsoft.VSCode.Dsc/VSCodeExtension.md @@ -35,7 +35,7 @@ The `VSCodeExtension` DSC Resource allows you to install, update, and remove Vis $params = @{ Name = 'ms-python.python' } -Invoke-DscResource -Name VSCodeExtension -Method Set -Property $params -ModuleName Microsoft.VSCode.Dsc +Invoke-DscResource -ModuleName Microsoft.VSCode.Dsc -Name VSCodeExtension -Method Set -Property $params ``` ### EXAMPLE 2 @@ -46,7 +46,7 @@ $params = @{ Name = 'ms-python.python' Version = '2021.5.842923320' } -Invoke-DscResource -Name VSCodeExtension -Method Set -Property $params -ModuleName Microsoft.VSCode.Dsc +Invoke-DscResource -ModuleName Microsoft.VSCode.Dsc -Name VSCodeExtension -Method Set -Property $params ``` ### EXAMPLE 3 @@ -57,7 +57,7 @@ $params = @{ Name = 'ms-python.python' Exist = $false } -Invoke-DscResource -Name VSCodeExtension -Method Set -Property $params -ModuleName Microsoft.VSCode.Dsc +Invoke-DscResource -ModuleName Microsoft.VSCode.Dsc -Name VSCodeExtension -Method Set -Property $params ``` ### EXAMPLE 4 @@ -68,5 +68,5 @@ $params = @{ Name = 'ms-python.python' Insiders = $true } -Invoke-DscResource -Name VSCodeExtension -Method Set -Property $params -ModuleName Microsoft.VSCode.Dsc +Invoke-DscResource -ModuleName Microsoft.VSCode.Dsc -Name VSCodeExtension -Method Set -Property $params ``` diff --git a/tests/QA/module.tests.ps1 b/tests/QA/module.tests.ps1 index 2e872e4f..41e612f8 100644 --- a/tests/QA/module.tests.ps1 +++ b/tests/QA/module.tests.ps1 @@ -11,6 +11,157 @@ param ( Write-Verbose ("repoRootPath: $repoRootPath") -Verbose Write-Verbose ("modules: $($modules.Count)") -Verbose +#region Functions +function Get-MarkdownHeadings { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string]$FilePath + ) + + $fileContent = Get-Content -Path $FilePath + + $headings = @() + + # Use pattern to capture all headings + $headingPattern = '^(#+)\s+(.*)' + + foreach ($line in $fileContent) { + if ($line -match $headingPattern) { + $level = $matches[1].Length + $text = $matches[2] + + $heading = [PSCustomObject]@{ + Level = $level + Text = $text + } + + $headings += $heading + } + } + + return $headings +} + +function Get-MdCodeBlock { + [CmdletBinding()] + [OutputType([CodeBlock])] + param ( + [Parameter(Mandatory, ValueFromPipeline, Position = 0)] + [string[]] + [SupportsWildcards()] + $Path, + + [Parameter()] + [string] + $BasePath = '.', + + [Parameter()] + [string] + $Language + ) + + process { + foreach ($unresolved in $Path) { + foreach ($file in (Resolve-Path -Path $unresolved).Path) { + $file = (Resolve-Path -Path $file).Path + $BasePath = (Resolve-Path -Path $BasePath).Path + $escapedRoot = [regex]::Escape($BasePath) + $relativePath = $file -replace "$escapedRoot\\", '' + + + # This section imports files referenced by PyMdown snippet syntax + # Example: --8<-- "abbreviations.md" + # Note: This function only supports very basic snippet syntax. + # See https://facelessuser.github.io/pymdown-extensions/extensions/snippets/ for documentation on the Snippets PyMdown extension + $lines = [System.IO.File]::ReadAllLines($file, [System.Text.Encoding]::UTF8) | ForEach-Object { + if ($_ -match '--8<-- "(?[^"]+)"') { + $snippetPath = Join-Path -Path $BasePath -ChildPath $Matches.file + if (Test-Path -Path $snippetPath) { + Get-Content -Path $snippetPath + } else { + Write-Warning "Snippet not found: $snippetPath" + } + } else { + $_ + } + } + + + $lineNumber = 0 + $code = $null + $state = [MdState]::Undefined + $content = [System.Text.StringBuilder]::new() + + foreach ($line in $lines) { + $lineNumber++ + switch ($state) { + 'Undefined' { + if ($line -match '^\s*```(?\w+)?' -and ([string]::IsNullOrWhiteSpace($Language) -or $Matches.lang -eq $Language)) { + $state = [MdState]::InCodeBlock + $code = [CodeBlock]@{ + Source = $relativePath + Language = $Matches.lang + LineNumber = $lineNumber + } + } elseif (($inlineMatches = [regex]::Matches($line, '(?\w+) )?(?[^`]+)`(?!`)'))) { + if (-not [string]::IsNullOrWhiteSpace($Language) -and $inlineMatch.Groups.lang -ne $Language) { + continue + } + foreach ($inlineMatch in $inlineMatches) { + [CodeBlock]@{ + Source = $relativePath + Language = $inlineMatch.Groups.lang + Content = $inlineMatch.Groups.code + LineNumber = $lineNumber + Position = $inlineMatch.Index + Inline = $true + } + } + } + } + + 'InCodeBlock' { + if ($line -match '^\s*```') { + $state = [MdState]::Undefined + $code.Content = $content.ToString() + $code + $code = $null + $null = $content.Clear() + } else { + $null = $content.AppendLine($line) + } + } + } + } + } + } + } +} +#endRegion Functions + +#region Enum +enum MdState { + Undefined + InCodeBlock +} +#endRegion Enum +class CodeBlock { + [string] $Source + [string] $Language + [string] $Content + [int] $LineNumber + [int] $Position + [bool] $Inline + + [string] ToString() { + return '{0}:{1}:{2}' -f $this.Source, $this.LineNumber, $this.Language + } +} +#region Classes + +#endRegion Classes + BeforeDiscovery { $moduleResources = [System.Collections.ArrayList]@() @@ -86,13 +237,39 @@ Describe 'Module tests' { $moduleResource = $_ $moduleImport = Import-PowerShellDataFile -Path $moduleResource.ModulePath.Replace('.psm1', '.psd1') + # For the resources $resources = [System.Collections.ArrayList]@() + # For the code blocks to capture in the examples + $codeBlocks = [System.Collections.ArrayList]@() + foreach ($resource in $moduleImport.DscResourcesToExport) { + $helpFile = Join-Path $repoRootPath 'resources' 'Help' $moduleResource.ModuleName "$resource.md" + $resources += @{ moduleName = $moduleResource.ModuleName resource = $resource - HelpFile = Join-Path $repoRootPath 'resources' 'Help' $moduleResource.ModuleName "$resource.md" + helpFile = $helpFile + CodeBlock = Get-MdCodeBlock -Path $helpFile -Language 'powershell' -ErrorAction SilentlyContinue + } + + $blocks = Get-MdCodeBlock -Path $helpFile -Language 'powershell' -ErrorAction SilentlyContinue + if (-not $blocks) { + $codeBlocks += @{ + moduleName = $moduleResource.ModuleName + resource = $resource + content = 'No code block found' + language = 'powershell' + } + } + + foreach ($block in $blocks) { + $codeBlocks += @{ + moduleName = $moduleResource.ModuleName + resource = $resource + content = $block.Content + language = $block.Language + } } } @@ -117,5 +294,94 @@ Describe 'Module tests' { $file = Get-Item -Path $helpFile -ErrorAction SilentlyContinue $file.Length | Should -BeGreaterThan 0 } + + It '[] Should have a help file for [] resource with heading 1' -TestCases $resources { + param ( + [string] $moduleName, + [string] $resource, + [string] $helpFile + ) + + $headings = Get-MarkdownHeadings -FilePath $helpFile -ErrorAction SilentlyContinue + + $h1 = $headings | Where-Object { $_.Level -eq 1 -and $_.Text -eq $moduleName } + $h1 | Should -Not -BeNullOrEmpty + } + + It '[] Should have a help file for [] resource with heading 2 matching SYNOPSIS' -TestCases $resources { + param ( + [string] $moduleName, + [string] $resource, + [string] $helpFile + ) + + $headings = Get-MarkdownHeadings -FilePath $helpFile -ErrorAction SilentlyContinue + + $h2 = $headings | Where-Object { $_.Level -eq 2 -and $_.Text -eq 'SYNOPSIS' } + $h2 | Should -Not -BeNullOrEmpty + } + + It '[] Should have a help file for [] resource with heading 2 matching DESCRIPTION' -TestCases $resources { + param ( + [string] $moduleName, + [string] $resource, + [string] $helpFile + ) + + $headings = Get-MarkdownHeadings -FilePath $helpFile -ErrorAction SilentlyContinue + + $h2 = $headings | Where-Object { $_.Level -eq 2 -and $_.Text -eq 'DESCRIPTION' } + $h2 | Should -Not -BeNullOrEmpty + } + + It '[] Should have a help file for [] resource with heading 2 matching PARAMETERS' -TestCases $resources { + param ( + [string] $moduleName, + [string] $resource, + [string] $helpFile + ) + + $headings = Get-MarkdownHeadings -FilePath $helpFile -ErrorAction SilentlyContinue + + $h2 = $headings | Where-Object { $_.Level -eq 2 -and $_.Text -eq 'PARAMETERS' } + $h2 | Should -Not -BeNullOrEmpty + } + + It '[] Should have a help file for [] resource with heading 2 matching EXAMPLES' -TestCases $resources { + param ( + [string] $moduleName, + [string] $resource, + [string] $helpFile + ) + + $headings = Get-MarkdownHeadings -FilePath $helpFile -ErrorAction SilentlyContinue + + $h2 = $headings | Where-Object { $_.Level -eq 2 -and $_.Text -eq 'EXAMPLES' } + $h2 | Should -Not -BeNullOrEmpty + } + + It '[] Should have a help file for [] with 1 example' -TestCases $resources { + param ( + [string] $moduleName, + [string] $resource, + [string] $helpFile + ) + + $headings = Get-MarkdownHeadings -FilePath $helpFile -ErrorAction SilentlyContinue + + $h3 = $headings | Where-Object { $_.Level -eq 3 -and $_.Text -eq 'EXAMPLE 1' } + $h3 | Should -Not -BeNullOrEmpty + } + + It '[] Should have at least a PowerShell coding example with Invoke-DscResource' -TestCases $codeBlocks { + param ( + [string] $ModuleName, + [string] $Content, + [string] $Language + ) + + $Content | Should -Match "Invoke-DscResource -ModuleName $ModuleName -Name $ResourceName" + } } } +