@@ -11,6 +11,157 @@ param (
1111Write-Verbose (" repoRootPath: $repoRootPath " ) - Verbose
1212Write-Verbose (" modules: $ ( $modules.Count ) " ) - Verbose
1313
14+ # region Functions
15+ function Get-MarkdownHeadings {
16+ [CmdletBinding ()]
17+ param (
18+ [Parameter (Mandatory = $true )]
19+ [string ]$FilePath
20+ )
21+
22+ $fileContent = Get-Content - Path $FilePath
23+
24+ $headings = @ ()
25+
26+ # Use pattern to capture all headings
27+ $headingPattern = ' ^(#+)\s+(.*)'
28+
29+ foreach ($line in $fileContent ) {
30+ if ($line -match $headingPattern ) {
31+ $level = $matches [1 ].Length
32+ $text = $matches [2 ]
33+
34+ $heading = [PSCustomObject ]@ {
35+ Level = $level
36+ Text = $text
37+ }
38+
39+ $headings += $heading
40+ }
41+ }
42+
43+ return $headings
44+ }
45+
46+ function Get-MdCodeBlock {
47+ [CmdletBinding ()]
48+ [OutputType ([CodeBlock ])]
49+ param (
50+ [Parameter (Mandatory , ValueFromPipeline , Position = 0 )]
51+ [string []]
52+ [SupportsWildcards ()]
53+ $Path ,
54+
55+ [Parameter ()]
56+ [string ]
57+ $BasePath = ' .' ,
58+
59+ [Parameter ()]
60+ [string ]
61+ $Language
62+ )
63+
64+ process {
65+ foreach ($unresolved in $Path ) {
66+ foreach ($file in (Resolve-Path - Path $unresolved ).Path) {
67+ $file = (Resolve-Path - Path $file ).Path
68+ $BasePath = (Resolve-Path - Path $BasePath ).Path
69+ $escapedRoot = [regex ]::Escape($BasePath )
70+ $relativePath = $file -replace " $escapedRoot \\" , ' '
71+
72+
73+ # This section imports files referenced by PyMdown snippet syntax
74+ # Example: --8<-- "abbreviations.md"
75+ # Note: This function only supports very basic snippet syntax.
76+ # See https://facelessuser.github.io/pymdown-extensions/extensions/snippets/ for documentation on the Snippets PyMdown extension
77+ $lines = [System.IO.File ]::ReadAllLines($file , [System.Text.Encoding ]::UTF8) | ForEach-Object {
78+ if ($_ -match ' --8<-- "(?<file>[^"]+)"' ) {
79+ $snippetPath = Join-Path - Path $BasePath - ChildPath $Matches.file
80+ if (Test-Path - Path $snippetPath ) {
81+ Get-Content - Path $snippetPath
82+ } else {
83+ Write-Warning " Snippet not found: $snippetPath "
84+ }
85+ } else {
86+ $_
87+ }
88+ }
89+
90+
91+ $lineNumber = 0
92+ $code = $null
93+ $state = [MdState ]::Undefined
94+ $content = [System.Text.StringBuilder ]::new()
95+
96+ foreach ($line in $lines ) {
97+ $lineNumber ++
98+ switch ($state ) {
99+ ' Undefined' {
100+ if ($line -match ' ^\s*```(?<lang>\w+)?' -and ([string ]::IsNullOrWhiteSpace($Language ) -or $Matches.lang -eq $Language )) {
101+ $state = [MdState ]::InCodeBlock
102+ $code = [CodeBlock ]@ {
103+ Source = $relativePath
104+ Language = $Matches.lang
105+ LineNumber = $lineNumber
106+ }
107+ } elseif (($inlineMatches = [regex ]::Matches($line , ' (?<!`)`(#!(?<lang>\w+) )?(?<code>[^`]+)`(?!`)' ))) {
108+ if (-not [string ]::IsNullOrWhiteSpace($Language ) -and $inlineMatch.Groups.lang -ne $Language ) {
109+ continue
110+ }
111+ foreach ($inlineMatch in $inlineMatches ) {
112+ [CodeBlock ]@ {
113+ Source = $relativePath
114+ Language = $inlineMatch.Groups.lang
115+ Content = $inlineMatch.Groups.code
116+ LineNumber = $lineNumber
117+ Position = $inlineMatch.Index
118+ Inline = $true
119+ }
120+ }
121+ }
122+ }
123+
124+ ' InCodeBlock' {
125+ if ($line -match ' ^\s*```' ) {
126+ $state = [MdState ]::Undefined
127+ $code.Content = $content.ToString ()
128+ $code
129+ $code = $null
130+ $null = $content.Clear ()
131+ } else {
132+ $null = $content.AppendLine ($line )
133+ }
134+ }
135+ }
136+ }
137+ }
138+ }
139+ }
140+ }
141+ # endRegion Functions
142+
143+ # region Enum
144+ enum MdState {
145+ Undefined
146+ InCodeBlock
147+ }
148+ # endRegion Enum
149+ class CodeBlock {
150+ [string ] $Source
151+ [string ] $Language
152+ [string ] $Content
153+ [int ] $LineNumber
154+ [int ] $Position
155+ [bool ] $Inline
156+
157+ [string ] ToString() {
158+ return ' {0}:{1}:{2}' -f $this.Source , $this.LineNumber , $this.Language
159+ }
160+ }
161+ # region Classes
162+
163+ # endRegion Classes
164+
14165BeforeDiscovery {
15166 $moduleResources = [System.Collections.ArrayList ]@ ()
16167
@@ -86,13 +237,39 @@ Describe 'Module tests' {
86237 $moduleResource = $_
87238 $moduleImport = Import-PowerShellDataFile - Path $moduleResource.ModulePath.Replace (' .psm1' , ' .psd1' )
88239
240+ # For the resources
89241 $resources = [System.Collections.ArrayList ]@ ()
90242
243+ # For the code blocks to capture in the examples
244+ $codeBlocks = [System.Collections.ArrayList ]@ ()
245+
91246 foreach ($resource in $moduleImport.DscResourcesToExport ) {
247+ $helpFile = Join-Path $repoRootPath ' resources' ' Help' $moduleResource.ModuleName " $resource .md"
248+
92249 $resources += @ {
93250 moduleName = $moduleResource.ModuleName
94251 resource = $resource
95- HelpFile = Join-Path $repoRootPath ' resources' ' Help' $moduleResource.ModuleName " $resource .md"
252+ helpFile = $helpFile
253+ CodeBlock = Get-MdCodeBlock - Path $helpFile - Language ' powershell' - ErrorAction SilentlyContinue
254+ }
255+
256+ $blocks = Get-MdCodeBlock - Path $helpFile - Language ' powershell' - ErrorAction SilentlyContinue
257+ if (-not $blocks ) {
258+ $codeBlocks += @ {
259+ moduleName = $moduleResource.ModuleName
260+ resource = $resource
261+ content = ' No code block found'
262+ language = ' powershell'
263+ }
264+ }
265+
266+ foreach ($block in $blocks ) {
267+ $codeBlocks += @ {
268+ moduleName = $moduleResource.ModuleName
269+ resource = $resource
270+ content = $block.Content
271+ language = $block.Language
272+ }
96273 }
97274 }
98275
@@ -117,5 +294,94 @@ Describe 'Module tests' {
117294 $file = Get-Item - Path $helpFile - ErrorAction SilentlyContinue
118295 $file.Length | Should - BeGreaterThan 0
119296 }
297+
298+ It ' [<moduleName>] Should have a help file for [<resource>] resource with heading 1' - TestCases $resources {
299+ param (
300+ [string ] $moduleName ,
301+ [string ] $resource ,
302+ [string ] $helpFile
303+ )
304+
305+ $headings = Get-MarkdownHeadings - FilePath $helpFile - ErrorAction SilentlyContinue
306+
307+ $h1 = $headings | Where-Object { $_.Level -eq 1 -and $_.Text -eq $moduleName }
308+ $h1 | Should -Not - BeNullOrEmpty
309+ }
310+
311+ It ' [<moduleName>] Should have a help file for [<resource>] resource with heading 2 matching SYNOPSIS' - TestCases $resources {
312+ param (
313+ [string ] $moduleName ,
314+ [string ] $resource ,
315+ [string ] $helpFile
316+ )
317+
318+ $headings = Get-MarkdownHeadings - FilePath $helpFile - ErrorAction SilentlyContinue
319+
320+ $h2 = $headings | Where-Object { $_.Level -eq 2 -and $_.Text -eq ' SYNOPSIS' }
321+ $h2 | Should -Not - BeNullOrEmpty
322+ }
323+
324+ It ' [<moduleName>] Should have a help file for [<resource>] resource with heading 2 matching DESCRIPTION' - TestCases $resources {
325+ param (
326+ [string ] $moduleName ,
327+ [string ] $resource ,
328+ [string ] $helpFile
329+ )
330+
331+ $headings = Get-MarkdownHeadings - FilePath $helpFile - ErrorAction SilentlyContinue
332+
333+ $h2 = $headings | Where-Object { $_.Level -eq 2 -and $_.Text -eq ' DESCRIPTION' }
334+ $h2 | Should -Not - BeNullOrEmpty
335+ }
336+
337+ It ' [<moduleName>] Should have a help file for [<resource>] resource with heading 2 matching PARAMETERS' - TestCases $resources {
338+ param (
339+ [string ] $moduleName ,
340+ [string ] $resource ,
341+ [string ] $helpFile
342+ )
343+
344+ $headings = Get-MarkdownHeadings - FilePath $helpFile - ErrorAction SilentlyContinue
345+
346+ $h2 = $headings | Where-Object { $_.Level -eq 2 -and $_.Text -eq ' PARAMETERS' }
347+ $h2 | Should -Not - BeNullOrEmpty
348+ }
349+
350+ It ' [<moduleName>] Should have a help file for [<resource>] resource with heading 2 matching EXAMPLES' - TestCases $resources {
351+ param (
352+ [string ] $moduleName ,
353+ [string ] $resource ,
354+ [string ] $helpFile
355+ )
356+
357+ $headings = Get-MarkdownHeadings - FilePath $helpFile - ErrorAction SilentlyContinue
358+
359+ $h2 = $headings | Where-Object { $_.Level -eq 2 -and $_.Text -eq ' EXAMPLES' }
360+ $h2 | Should -Not - BeNullOrEmpty
361+ }
362+
363+ It ' [<moduleName>] Should have a help file for [<resource>] with 1 example' - TestCases $resources {
364+ param (
365+ [string ] $moduleName ,
366+ [string ] $resource ,
367+ [string ] $helpFile
368+ )
369+
370+ $headings = Get-MarkdownHeadings - FilePath $helpFile - ErrorAction SilentlyContinue
371+
372+ $h3 = $headings | Where-Object { $_.Level -eq 3 -and $_.Text -eq ' EXAMPLE 1' }
373+ $h3 | Should -Not - BeNullOrEmpty
374+ }
375+
376+ It ' [<moduleName>] Should have at least a PowerShell coding example with Invoke-DscResource' - TestCases $codeBlocks {
377+ param (
378+ [string ] $ModuleName ,
379+ [string ] $Content ,
380+ [string ] $Language
381+ )
382+
383+ $Content | Should -Match " Invoke-DscResource -ModuleName $ModuleName -Name $ResourceName "
384+ }
120385 }
121386}
387+
0 commit comments