diff --git a/CHANGELOG.md b/CHANGELOG.md index 315186a..ac561fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Added private functions: + - `Get-ClassAst` - Returns the AST for a single or all classes. + - `Get-ClassResourceAst` - Returns the AST for a single or all DSC class + resources. + - `Get-ClassResourceProperty` - Returns DSC class resource properties + from the provided class or classes. + - `Format-Text` - Format a string according to predefined options. + +### Changed + +- `New-DscResourceWikiPage` + - If a class-based resource has a parent class that contains DSC resource + properties they will now also be returned as part of the DSC resource + parameters ([issue #62](https://github.com/dsccommunity/DscResource.DocGenerator/issues/62)). + ### Fixed - `Publish_GitHub_Wiki_Content` diff --git a/source/Private/Format-Text.ps1 b/source/Private/Format-Text.ps1 new file mode 100644 index 0000000..78007e8 --- /dev/null +++ b/source/Private/Format-Text.ps1 @@ -0,0 +1,83 @@ +<# + .SYNOPSIS + Formats a string using predefined format options. + + .PARAMETER Text + The string to format. + + .PARAMETER Format + One or more predefined format options. The formatting is done in the + provided order. + + .EXAMPLE + Format-Text -Text 'My text description' -Format @('Replace_Multiple_Whitespace_With_One') + + Returns a string correctly formatted with one whitespace between each word. +#> +function Format-Text +{ + [CmdletBinding()] + [OutputType([System.String])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Text, + + [Parameter(Mandatory = $true)] + [ValidateSet( + 'Replace_Multiple_Whitespace_With_One', + 'Remove_Blank_Rows_At_End_Of_String', + 'Remove_Indentation_From_Blank_Rows', + 'Replace_NewLine_With_One_Whitespace', + 'Replace_Vertical_Bar_With_One_Whitespace', + 'Remove_Whitespace_From_End_Of_String' + )] + [System.String[]] + $Format + ) + + $returnString = $Text + + switch ($Format) + { + # Replace multiple whitespace with one single white space + 'Replace_Multiple_Whitespace_With_One' + { + $returnString = $returnString -replace ' +', ' ' + } + + # Removes all blank rows at the end + 'Remove_Blank_Rows_At_End_Of_String' + { + $returnString = $returnString -replace '[\r?\n]+$' + } + + # Remove all indentations from blank rows + 'Remove_Indentation_From_Blank_Rows' + { + $returnString = $returnString -replace '[ ]+\r\n', "`r`n" + $returnString = $returnString -replace '[ ]+\n', "`n" + } + + # Replace LF or CRLF with one white space + 'Replace_NewLine_With_One_Whitespace' + { + $returnString = $returnString -replace '\r?\n', ' ' + } + + # Replace vertical bar with one white space + 'Replace_Vertical_Bar_With_One_Whitespace' + { + $returnString = $returnString -replace '\|', ' ' + } + + # Remove white space from end of string + 'Remove_Whitespace_From_End_Of_String' + { + $returnString = $returnString -replace ' +$' + } + } + + return $returnString +} diff --git a/source/Private/Get-ClassAst.ps1 b/source/Private/Get-ClassAst.ps1 new file mode 100644 index 0000000..2ceef9f --- /dev/null +++ b/source/Private/Get-ClassAst.ps1 @@ -0,0 +1,65 @@ +<# + .SYNOPSIS + Returns the AST for a single or all classes. + + .PARAMETER ScriptFile + The path to the source file that contain the class. + + .PARAMETER ClassName + The specific class to return the AST for. Optional. + + .EXAMPLE + Get-ClassAst -ClassName 'myClass' -ScriptFile '.\output\MyModule\1.0.0\MyModule.psm1' + + Returns AST for all the classes in the script file. + + .EXAMPLE + Get-ClassAst -ClassName 'myClass' -ScriptFile '.\output\MyModule\1.0.0\MyModule.psm1' + + Returns AST for the class 'myClass' from the script file. +#> +function Get-ClassAst +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $ScriptFile, + + [Parameter()] + [System.String] + $ClassName + ) + + $tokens, $parseErrors = $null + + $ast = [System.Management.Automation.Language.Parser]::ParseFile($ScriptFile, [ref] $tokens, [ref] $parseErrors) + + if ($parseErrors) + { + throw $parseErrors + } + + if ($PSBoundParameters.ContainsKey('ClassName') -and $ClassName) + { + # Get only the specific class resource. + $astFilter = { + $args[0] -is [System.Management.Automation.Language.TypeDefinitionAst] ` + -and $args[0].IsClass ` + -and $args[0].Name -eq $ClassName + } + } + else + { + # Get all class resources. + $astFilter = { + $args[0] -is [System.Management.Automation.Language.TypeDefinitionAst] ` + -and $args[0].IsClass + } + } + + $classAst = $ast.FindAll($astFilter, $true) + + return $classAst +} diff --git a/source/Private/Get-ClassResourceAst.ps1 b/source/Private/Get-ClassResourceAst.ps1 new file mode 100644 index 0000000..b98facf --- /dev/null +++ b/source/Private/Get-ClassResourceAst.ps1 @@ -0,0 +1,62 @@ +<# + .SYNOPSIS + Returns the AST for a single or all DSC class resources. + + .PARAMETER ScriptFile + The path to the source file that contain the DSC class resource. + + .PARAMETER ClassName + The specific DSC class resource to return the AST for. Optional. + + .EXAMPLE + Get-ClassResourceAst -ClassName 'myClass' -ScriptFile '.\output\MyModule\1.0.0\MyModule.psm1' + + Returns AST for all DSC class resources in the script file. + + .EXAMPLE + Get-ClassResourceAst -ClassName 'myClass' -ScriptFile '.\output\MyModule\1.0.0\MyModule.psm1' + + Returns AST for the DSC class resource 'myClass' from the script file. +#> +function Get-ClassResourceAst +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $ScriptFile, + + [Parameter()] + [System.String] + $ClassName + ) + + $dscClassResourceAst = $null + + $getClassAstParameters = @{ + ScriptFile = $ScriptFile + } + + if ($PSBoundParameters.ContainsKey('ClassName')) + { + $getClassAstParameters['ClassName'] = $ClassName + } + + $ast = Get-ClassAst @getClassAstParameters + + # Only try to filter if there was at least one class returned. + if ($ast) + { + # Get only DSC class resource. + $astFilter = { + $args[0] -is [System.Management.Automation.Language.TypeDefinitionAst] ` + -and $args[0].IsClass ` + -and $args[0].Attributes.Extent.Text -imatch '\[DscResource\(.*\)\]' + } + + $dscClassResourceAst = $ast.FindAll($astFilter, $true) + } + + return $dscClassResourceAst +} diff --git a/source/Private/Get-ClassResourceProperty.ps1 b/source/Private/Get-ClassResourceProperty.ps1 new file mode 100644 index 0000000..a3cf7f2 --- /dev/null +++ b/source/Private/Get-ClassResourceProperty.ps1 @@ -0,0 +1,127 @@ +<# + .SYNOPSIS + Returns DSC class resource properties from the provided class or classes. + + .PARAMETER SourcePath + The path to the source folder (in which the child folder 'Classes' exist). + + .PARAMETER BuiltModuleScriptFilePath + The path to the built module script file that contains the class. + + .PARAMETER ClassName + One or more class names to return properties for. + + .EXAMPLE + Get-ClassResourceProperty -ClassName @('myParentClass', 'myClass') -BuiltModuleScriptFilePath '.\output\MyModule\1.0.0\MyModule.psm1' -SourcePath '.\source' + + Returns all DSC class resource properties. +#> +function Get-ClassResourceProperty +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable[]])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $SourcePath, + + [Parameter(Mandatory = $true)] + [System.String] + $BuiltModuleScriptFilePath, + + [Parameter(Mandatory = $true)] + [System.String[]] + $ClassName + ) + + $resourceProperty = [System.Collections.Hashtable[]] @() + + foreach ($currentClassName in $ClassName) + { + $dscResourceAst = Get-ClassAst -ClassName $currentClassName -ScriptFile $BuiltModuleScriptFilePath + + $sourceFilePath = Join-Path -Path $SourcePath -ChildPath ('Classes/*{0}.ps1' -f $currentClassName) + + $dscResourceCommentBasedHelp = Get-ClassResourceCommentBasedHelp -Path $sourceFilePath + + $astFilter = { + $args[0] -is [System.Management.Automation.Language.PropertyMemberAst] ` + -and $args[0].Attributes.TypeName.Name -eq 'DscProperty' + } + + $propertyMemberAsts = $dscResourceAst.FindAll($astFilter, $true) + + <# + Looping through each resource property to build the resulting + hashtable. Hashtable will be in the format: + + @{ + Name = + State = 'Key' | 'Required' |'Write' | 'Read' + Description = + EmbeddedInstance = 'MSFT_Credential' | $null + DataType = 'System.String' | 'System.String[] | etc. + IsArray = $true | $false + ValueMap = @( | ...) + } + #> + foreach ($propertyMemberAst in $propertyMemberAsts) + { + Write-Verbose -Message ($script:localizedData.FoundClassResourcePropertyMessage -f $propertyMemberAst.Name, $dscResourceAst.Name) + + $propertyAttribute = @{ + Name = $propertyMemberAst.Name + DataType = $propertyMemberAst.PropertyType.TypeName.FullName + + # Always set to null, correct type name is set in DataType. + EmbeddedInstance = $null + + # Always set to $false - correct type name is set in DataType. + IsArray = $false + } + + $propertyAttribute['State'] = Get-ClassResourcePropertyState -Ast $propertyMemberAst + + $astFilter = { + $args[0] -is [System.Management.Automation.Language.AttributeAst] ` + -and $args[0].TypeName.Name -eq 'ValidateSet' + } + + $propertyAttributeAsts = $propertyMemberAst.FindAll($astFilter, $true) + + if ($propertyAttributeAsts) + { + $propertyAttribute['ValueMap'] = $propertyAttributeAsts.PositionalArguments.Value + } + + if ($dscResourceCommentBasedHelp -and $dscResourceCommentBasedHelp.Parameters.Count -gt 0) + { + # The key name must be upper-case for it to match the right item in the list of parameters. + $propertyDescription = $dscResourceCommentBasedHelp.Parameters[$propertyMemberAst.Name.ToUpper()] + + if ($propertyDescription) + { + $propertyDescription = Format-Text -Text $propertyDescription -Format @( + 'Remove_Blank_Rows_At_End_Of_String', + 'Remove_Indentation_From_Blank_Rows', + 'Replace_NewLine_With_One_Whitespace', + 'Replace_Vertical_Bar_With_One_Whitespace', + 'Replace_Multiple_Whitespace_With_One', + 'Remove_Whitespace_From_End_Of_String' + ) + } + } + else + { + $propertyDescription = '' + } + + $propertyAttribute['Description'] = $propertyDescription + + $resourceProperty += $propertyAttribute + } + } + + return $resourceProperty +} diff --git a/source/Public/New-DscResourceWikiPage.ps1 b/source/Public/New-DscResourceWikiPage.ps1 index 05c81f3..148fa33 100644 --- a/source/Public/New-DscResourceWikiPage.ps1 +++ b/source/Public/New-DscResourceWikiPage.ps1 @@ -178,22 +178,7 @@ function New-DscResourceWikiPage # Looping through each module file (normally just one). foreach ($builtModuleScriptFile in $builtModuleScriptFiles) { - $tokens, $parseErrors = $null - - $ast = [System.Management.Automation.Language.Parser]::ParseFile($builtModuleScriptFile.FullName, [ref] $tokens, [ref] $parseErrors) - - if ($parseErrors) - { - throw $parseErrors - } - - $astFilter = { - $args[0] -is [System.Management.Automation.Language.TypeDefinitionAst] ` - -and $args[0].IsClass -eq $true ` - -and $args[0].Attributes.Extent.Text -imatch '\[DscResource\(.*\)\]' - } - - $dscResourceAsts = $ast.FindAll($astFilter, $true) + $dscResourceAsts = Get-ClassResourceAst -ScriptFile $builtModuleScriptFile.FullName Write-Verbose -Message ($script:localizedData.FoundClassBasedMessage -f $dscResourceAsts.Count, $builtModuleScriptFile.FullName) @@ -211,74 +196,17 @@ function New-DscResourceWikiPage $sourceFilePath = Join-Path -Path $SourcePath -ChildPath ('Classes/*{0}.ps1' -f $dscResourceAst.Name) - $dscResourceCommentBasedHelp = Get-ClassResourceCommentBasedHelp -Path $sourceFilePath - - $astFilter = { - $args[0] -is [System.Management.Automation.Language.PropertyMemberAst] - } + $className = @() - $propertyMemberAsts = $dscResourceAst.FindAll($astFilter, $true) - - $resourceProperty = @() - - <# - Looping through each resource property to build the hashtable - that should be passed to Get-DscResourceSchemaPropertyContent. - Hashtable should be in the format: - - @{ - Name = - State = 'Key' | 'Required' |'Write' | 'Read' - Description = - EmbeddedInstance = 'MSFT_Credential' | $null - DataType = 'System.String' | 'System.String[] | etc. - IsArray = $true | $false - ValueMap = @( | ...) - } - #> - foreach ($propertyMemberAst in $propertyMemberAsts) + if ($dscResourceAst.BaseTypes.Count -gt 0) { - Write-Verbose -Message ($script:localizedData.FoundClassResourcePropertyMessage -f $propertyMemberAst.Name, $dscResourceAst.Name) - - $propertyAttribute = @{ - Name = $propertyMemberAst.Name - DataType = $propertyMemberAst.PropertyType.TypeName.FullName - - # Always set to null, correct type name is set in DataType. - EmbeddedInstance = $null - - # Always set to $false - correct type name is set in DataType. - IsArray = $false - } - - $propertyAttribute['State'] = Get-ClassResourcePropertyState -Ast $propertyMemberAst - - $astFilter = { - $args[0] -is [System.Management.Automation.Language.AttributeAst] ` - -and $args[0].TypeName.Name -eq 'ValidateSet' - } - - $propertyAttributeAsts = $propertyMemberAst.FindAll($astFilter, $true) - - if ($propertyAttributeAsts) - { - $propertyAttribute['ValueMap'] = $propertyAttributeAsts.PositionalArguments.Value - } - - # The key name must be upper-case for it to match the right item in the list of parameters. - $propertyDescription = ($dscResourceCommentBasedHelp.Parameters[$propertyMemberAst.Name.ToUpper()] -replace '[\r|\n]+$') - - $propertyDescription = $propertyDescription -replace '[\r|\n]+$' # Removes all blank rows at the end - $propertyDescription = $propertyDescription -replace '[ ]+\r\n', "`r`n" # Remove indentation from blank rows - $propertyDescription = $propertyDescription -replace '\r?\n', " " # Replace CRLF with one white space - $propertyDescription = $propertyDescription -replace '\|', " " # Replace vertical bar with white space - $propertyDescription = $propertyDescription -replace ' +', " " # Replace multiple whitespace with one single white space - $propertyDescription = $propertyDescription -replace ' +$' # Remove white space from end of row + $className += @($dscResourceAst.BaseTypes.TypeName.Name) + } - $propertyAttribute['Description'] = $propertyDescription + $className += $dscResourceAst.Name - $resourceProperty += $propertyAttribute - } + # Returns the properties for class and any existing parent class(es). + $resourceProperty = Get-ClassResourceProperty -ClassName $className -SourcePath $SourcePath -BuiltModuleScriptFilePath $builtModuleScriptFile.FullName $propertyContent = Get-DscResourceSchemaPropertyContent -Property $resourceProperty -UseMarkdown @@ -289,6 +217,8 @@ function New-DscResourceWikiPage $null = $output.AppendLine() + $dscResourceCommentBasedHelp = Get-ClassResourceCommentBasedHelp -Path $sourceFilePath + $description = $dscResourceCommentBasedHelp.Description $description = $description -replace '[\r|\n]+$' # Removes all blank rows and whitespace at the end diff --git a/tests/unit/private/Format-Text.Tests.ps1 b/tests/unit/private/Format-Text.Tests.ps1 new file mode 100644 index 0000000..3b0afb2 --- /dev/null +++ b/tests/unit/private/Format-Text.Tests.ps1 @@ -0,0 +1,130 @@ +#region HEADER +$script:projectPath = "$PSScriptRoot\..\..\.." | Convert-Path +$script:projectName = (Get-ChildItem -Path "$script:projectPath\*\*.psd1" | Where-Object -FilterScript { + ($_.Directory.Name -match 'source|src' -or $_.Directory.Name -eq $_.BaseName) -and + $(try + { + Test-ModuleManifest -Path $_.FullName -ErrorAction Stop + } + catch + { + $false + }) + }).BaseName + +$script:moduleName = Get-Module -Name $script:projectName -ListAvailable | Select-Object -First 1 +Remove-Module -Name $script:moduleName -Force -ErrorAction 'SilentlyContinue' + +Import-Module $script:moduleName -Force -ErrorAction 'Stop' +#endregion HEADER + +InModuleScope $script:moduleName { + Describe 'Format-Text' { + It 'Should remove multiple whitespace and blank rows at end' { + $formatTextParameters = @{ + Text = "My text with multiple whitespace`r`n`r`n`r`n" + Format = @( + 'Replace_Multiple_Whitespace_With_One' + 'Remove_Blank_Rows_At_End_Of_String' + ) + } + + $result = Format-Text @formatTextParameters + + $result | Should -BeExactly 'My text with multiple whitespace' + } + + It 'Should remove indentations from blank rows when using CRLF' { + $formatTextParameters = @{ + Text = "First line`r`n `r`nSecond line" + Format = @( + 'Remove_Indentation_From_Blank_Rows' + ) + } + + $result = Format-Text @formatTextParameters + + $result | Should -BeExactly "First line`r`n`r`nSecond line" + } + + It 'Should remove indentations from blank rows when using LF' { + $formatTextParameters = @{ + Text = "First line`n `nSecond line" + Format = @( + 'Remove_Indentation_From_Blank_Rows' + ) + } + + $result = Format-Text @formatTextParameters + + $result | Should -BeExactly @" +First line`n`nSecond line +"@ + } + + It 'Should remove indentations from blank rows when using both LF and CRLF' { + $formatTextParameters = @{ + Text = "First line`n `n `r`nSecond line" + Format = @( + 'Remove_Indentation_From_Blank_Rows' + ) + } + + $result = Format-Text @formatTextParameters + + $result | Should -BeExactly "First line`n`n`r`nSecond line" + } + + It 'Should replace CRLF with one whitespace' { + $formatTextParameters = @{ + Text = "First line`r`nSecond line" + Format = @( + 'Replace_NewLine_With_One_Whitespace' + ) + } + + $result = Format-Text @formatTextParameters + + $result | Should -BeExactly 'First line Second line' + } + + It 'Should replace LF with one whitespace' { + $formatTextParameters = @{ + Text = "First line`nSecond line" + Format = @( + 'Replace_NewLine_With_One_Whitespace' + ) + } + + $result = Format-Text @formatTextParameters + + $result | Should -BeExactly 'First line Second line' + } + + It 'Should replace vertical bar with one whitespace' { + $formatTextParameters = @{ + Text = 'First|Second' + Format = @( + 'Replace_Vertical_Bar_With_One_Whitespace' + ) + } + + $result = Format-Text @formatTextParameters + + $result | Should -BeExactly 'First Second' + } + + It 'Should remove whitespace from end of string' { + $formatTextParameters = @{ + Text = 'First line ' + Format = @( + 'Remove_Whitespace_From_End_Of_String' + ) + } + + $result = Format-Text @formatTextParameters + + $result | Should -BeExactly 'First line' + } + } +} diff --git a/tests/unit/private/Get-ClassAst.Tests.ps1 b/tests/unit/private/Get-ClassAst.Tests.ps1 new file mode 100644 index 0000000..388fbfb --- /dev/null +++ b/tests/unit/private/Get-ClassAst.Tests.ps1 @@ -0,0 +1,120 @@ +#region HEADER +$script:projectPath = "$PSScriptRoot\..\..\.." | Convert-Path +$script:projectName = (Get-ChildItem -Path "$script:projectPath\*\*.psd1" | Where-Object -FilterScript { + ($_.Directory.Name -match 'source|src' -or $_.Directory.Name -eq $_.BaseName) -and + $(try + { + Test-ModuleManifest -Path $_.FullName -ErrorAction Stop + } + catch + { + $false + }) + }).BaseName + +$script:moduleName = Get-Module -Name $script:projectName -ListAvailable | Select-Object -First 1 +Remove-Module -Name $script:moduleName -Force -ErrorAction 'SilentlyContinue' + +Import-Module $script:moduleName -Force -ErrorAction 'Stop' +#endregion HEADER + +InModuleScope $script:moduleName { + Describe 'Get-ClassAst' { + Context 'When the script file cannot be parsed' { + BeforeAll { + $mockBuiltModulePath = Join-Path -Path $TestDrive -ChildPath 'output\MyClassModule\1.0.0' + + New-Item -Path $mockBuiltModulePath -ItemType 'Directory' -Force + + $mockBuiltModuleScriptFilePath = Join-Path -Path $mockBuiltModulePath -ChildPath 'MyClassModule.psm1' + + # The class DSC resource in the built module. + $mockBuiltModuleScript = @' +[DscResource()] +class MyDscResource +{ + [MyDscResource] Get() + { + return [MyDscResource] $this + } + + [System.Boolean] Test() + { + return $true + } + + [DscProperty(Key)] + [System.String] $ProjectName +} +'@ + + # Uses Microsoft.PowerShell.Utility\Out-File to override the stub that is needed for the mocks. + $mockBuiltModuleScript | Microsoft.PowerShell.Utility\Out-File -FilePath $mockBuiltModuleScriptFilePath -Encoding ascii -Force + } + + It 'Should throw an error' { + # This evaluates just part of the expected error message. + { Get-ClassAst -ScriptFile $mockBuiltModuleScriptFilePath } | Should -Throw "'MyDscResource' is missing a Set method" + } + } + + Context 'When the script file is parsed successfully' { + BeforeAll { + $mockBuiltModulePath = Join-Path -Path $TestDrive -ChildPath 'output\MyClassModule\1.0.0' + + New-Item -Path $mockBuiltModulePath -ItemType 'Directory' -Force + + $mockBuiltModuleScriptFilePath = Join-Path -Path $mockBuiltModulePath -ChildPath 'MyClassModule.psm1' + + # The class DSC resource in the built module. + $mockBuiltModuleScript = @' +class MyBaseClass +{ + [void] MyHelperFunction() {} +} + +[DscResource()] +class MyDscResource +{ + [MyDscResource] Get() + { + return [MyDscResource] $this + } + + [System.Boolean] Test() + { + return $true + } + + [void] Set() {} + + [DscProperty(Key)] + [System.String] $ProjectName +} +'@ + + # Uses Microsoft.PowerShell.Utility\Out-File to override the stub that is needed for the mocks. + $mockBuiltModuleScript | Microsoft.PowerShell.Utility\Out-File -FilePath $mockBuiltModuleScriptFilePath -Encoding ascii -Force + } + + Context 'When returning all classes in the script file' { + It 'Should return the correct classes' { + $astResult = Get-ClassAst -ScriptFile $mockBuiltModuleScriptFilePath + + $astResult | Should -HaveCount 2 + $astResult.Name | Should -Contain 'MyDscResource' + $astResult.Name | Should -Contain 'MyBaseClass' + } + } + + Context 'When returning a single class from the script file' { + It 'Should return the correct classes' { + $astResult = Get-ClassAst -ScriptFile $mockBuiltModuleScriptFilePath -ClassName 'MyBaseClass' + + $astResult | Should -HaveCount 1 + $astResult.Name | Should -Be 'MyBaseClass' + } + } + } + } +} diff --git a/tests/unit/private/Get-ClassResourceAst.Tests.ps1 b/tests/unit/private/Get-ClassResourceAst.Tests.ps1 new file mode 100644 index 0000000..4620951 --- /dev/null +++ b/tests/unit/private/Get-ClassResourceAst.Tests.ps1 @@ -0,0 +1,99 @@ +#region HEADER +$script:projectPath = "$PSScriptRoot\..\..\.." | Convert-Path +$script:projectName = (Get-ChildItem -Path "$script:projectPath\*\*.psd1" | Where-Object -FilterScript { + ($_.Directory.Name -match 'source|src' -or $_.Directory.Name -eq $_.BaseName) -and + $(try + { + Test-ModuleManifest -Path $_.FullName -ErrorAction Stop + } + catch + { + $false + }) + }).BaseName + +$script:moduleName = Get-Module -Name $script:projectName -ListAvailable | Select-Object -First 1 +Remove-Module -Name $script:moduleName -Force -ErrorAction 'SilentlyContinue' + +Import-Module $script:moduleName -Force -ErrorAction 'Stop' +#endregion HEADER + +InModuleScope $script:moduleName { + Describe 'Get-ClassResourceAst' { + BeforeAll { + $mockBuiltModulePath = Join-Path -Path $TestDrive -ChildPath 'output\MyClassModule\1.0.0' + + New-Item -Path $mockBuiltModulePath -ItemType 'Directory' -Force + + $mockBuiltModuleScriptFilePath = Join-Path -Path $mockBuiltModulePath -ChildPath 'MyClassModule.psm1' + + # The class DSC resource in the built module. + $mockBuiltModuleScript = @' +class MyBaseClass +{ + [void] MyHelperFunction() {} +} + +[DscResource()] +class AzDevOpsProject +{ + [AzDevOpsProject] Get() + { + return [AzDevOpsProject] $this + } + + [System.Boolean] Test() + { + return $true + } + + [void] Set() {} + + [DscProperty(Key)] + [System.String] $ProjectName +} + +[DscResource()] +class MyDscResource +{ + [MyDscResource] Get() + { + return [MyDscResource] $this + } + + [System.Boolean] Test() + { + return $true + } + + [void] Set() {} + + [DscProperty(Key)] + [System.String] $ProjectName +} +'@ + + # Uses Microsoft.PowerShell.Utility\Out-File to override the stub that is needed for the mocks. + $mockBuiltModuleScript | Microsoft.PowerShell.Utility\Out-File -FilePath $mockBuiltModuleScriptFilePath -Encoding ascii -Force + } + + Context 'When returning all DSC class resources in the script file' { + It 'Should return the correct classes' { + $astResult = Get-ClassResourceAst -ScriptFile $mockBuiltModuleScriptFilePath + + $astResult | Should -HaveCount 2 + $astResult.Name | Should -Contain 'MyDscResource' + $astResult.Name | Should -Contain 'AzDevOpsProject' + } + } + + Context 'When returning a single DSC class resource from the script file' { + It 'Should return the correct classes' { + $astResult = Get-ClassResourceAst -ScriptFile $mockBuiltModuleScriptFilePath -ClassName 'MyDscResource' + + $astResult | Should -HaveCount 1 + $astResult.Name | Should -Be 'MyDscResource' + } + } + } +} diff --git a/tests/unit/private/Get-ClassResourceProperty.Tests.ps1 b/tests/unit/private/Get-ClassResourceProperty.Tests.ps1 new file mode 100644 index 0000000..63acfe4 --- /dev/null +++ b/tests/unit/private/Get-ClassResourceProperty.Tests.ps1 @@ -0,0 +1,318 @@ +#region HEADER +$script:projectPath = "$PSScriptRoot\..\..\.." | Convert-Path +$script:projectName = (Get-ChildItem -Path "$script:projectPath\*\*.psd1" | Where-Object -FilterScript { + ($_.Directory.Name -match 'source|src' -or $_.Directory.Name -eq $_.BaseName) -and + $(try + { + Test-ModuleManifest -Path $_.FullName -ErrorAction Stop + } + catch + { + $false + }) + }).BaseName + +$script:moduleName = Get-Module -Name $script:projectName -ListAvailable | Select-Object -First 1 +Remove-Module -Name $script:moduleName -Force -ErrorAction 'SilentlyContinue' + +Import-Module $script:moduleName -Force -ErrorAction 'Stop' +#endregion HEADER + +InModuleScope $script:moduleName { + Describe 'Get-ClassResourceProperty' { + Context 'When the resource has a parent class that also has a DSC property' { + BeforeAll { + $mockBuiltModulePath = Join-Path -Path $TestDrive -ChildPath 'output\MyClassModule\1.0.0' + $mockSourcePath = Join-Path -Path $TestDrive -ChildPath 'source' + + New-Item -Path $mockBuiltModulePath -ItemType 'Directory' -Force + New-Item -Path "$mockSourcePath\Classes" -ItemType 'Directory' -Force + + # The class DSC resource in the built module. + $mockBuiltModuleScript = @' +class ResourceBase +{ + hidden [System.String] $NotADscProperty + + [DscProperty()] + [System.String] + $Ensure +} + +[DscResource()] +class MyDscResource : ResourceBase +{ + [MyDscResource] Get() + { + return [MyDscResource] $this + } + + [System.Boolean] Test() + { + return $true + } + + [void] Set() {} + + [DscProperty(Key)] + [System.String] $ProjectName + + [DscProperty()] + [ValidateSet('Up', 'Down')] + [System.String[]] $ValidateSetProperty +} +'@ + # Uses Microsoft.PowerShell.Utility\Out-File to override the stub that is needed for the mocks. + $mockBuiltModuleScript | Microsoft.PowerShell.Utility\Out-File -FilePath "$mockBuiltModulePath\MyClassModule.psm1" -Encoding ascii -Force + + <# + The source file of class DSC resource. This file is not actually + referencing the base class to simplify the tests. + The property ValidateSetProperty does not have a parameter description + to be able to test missing description. + #> + $mockResourceSourceScript = @' +<# + .SYNOPSIS + Resource synopsis. + + .DESCRIPTION + Resource description. + + .PARAMETER ProjectName + ProjectName description. +#> +[DscResource()] +class MyDscResource +{ + [MyDscResource] Get() + { + return [MyDscResource] $this + } + + [System.Boolean] Test() + { + return $true + } + + [void] Set() {} + + [DscProperty(Key)] + [System.String] $ProjectName + + [DscProperty()] + [ValidateSet('Up', 'Down')] + [System.String[]] $ValidateSetProperty +} +'@ + # Uses Microsoft.PowerShell.Utility\Out-File to override the stub that is needed for the mocks. + $mockResourceSourceScript | Microsoft.PowerShell.Utility\Out-File -FilePath "$mockSourcePath\Classes\010.MyDscResource.ps1" -Encoding ascii -Force + + $mockBaseClassSourceScript = @' +<# + .SYNOPSIS + Synopsis for base class. + + .DESCRIPTION + Description for base class + + .PARAMETER Ensure + Ensure description. +#> +class ResourceBase +{ + hidden [System.String] $NotADscProperty + + [DscProperty()] + [System.String] + $Ensure +} +'@ + # Uses Microsoft.PowerShell.Utility\Out-File to override the stub that is needed for the mocks. + $mockBaseClassSourceScript | Microsoft.PowerShell.Utility\Out-File -FilePath "$mockSourcePath\Classes\001.ResourceBase.ps1" -Encoding ascii -Force + } + + It 'Should return the expected DSC class resource properties' { + $mockGetClassResourcePropertyParameters = @{ + SourcePath = $mockSourcePath + BuiltModuleScriptFilePath = Join-Path -Path $mockBuiltModulePath -ChildPath 'MyClassModule.psm1' + ClassName = @( + 'ResourceBase' + 'MyDscResource' + ) + } + + $getClassResourcePropertyResult = Get-ClassResourceProperty @mockGetClassResourcePropertyParameters + $getClassResourcePropertyResult | Should -HaveCount 3 + $getClassResourcePropertyResult.Name | Should -Contain 'Ensure' + $getClassResourcePropertyResult.Name | Should -Contain 'ProjectName' + $getClassResourcePropertyResult.Name | Should -Contain 'ValidateSetProperty' + + $ensurePropertyResult = $getClassResourcePropertyResult.Where({$_.Name -eq 'Ensure'}) + $ensurePropertyResult.State | Should -Be 'Write' + $ensurePropertyResult.Description | Should -Be 'Ensure description.' + $ensurePropertyResult.DataType | Should -Be 'System.String' + $ensurePropertyResult.IsArray | Should -BeFalse + $ensurePropertyResult.ValueMap | Should -BeNullOrEmpty + + $ensurePropertyResult = $getClassResourcePropertyResult.Where({$_.Name -eq 'ProjectName'}) + $ensurePropertyResult.State | Should -Be 'Key' + $ensurePropertyResult.Description | Should -Be 'ProjectName description.' + $ensurePropertyResult.DataType | Should -Be 'System.String' + $ensurePropertyResult.IsArray | Should -BeFalse + $ensurePropertyResult.ValueMap | Should -BeNullOrEmpty + + $ensurePropertyResult = $getClassResourcePropertyResult.Where({$_.Name -eq 'ValidateSetProperty'}) + $ensurePropertyResult.State | Should -Be 'Write' + $ensurePropertyResult.Description | Should -BeNullOrEmpty + $ensurePropertyResult.DataType | Should -Be 'System.String[]' + $ensurePropertyResult.IsArray | Should -BeFalse + $ensurePropertyResult.ValueMap | Should -Contain 'Up' + $ensurePropertyResult.ValueMap | Should -Contain 'Down' + } + } + } + + Context 'When a base class is missing comment-based help' { + BeforeAll { + $mockBuiltModulePath = Join-Path -Path $TestDrive -ChildPath 'output\MyClassModule\1.0.0' + $mockSourcePath = Join-Path -Path $TestDrive -ChildPath 'source' + + New-Item -Path $mockBuiltModulePath -ItemType 'Directory' -Force + New-Item -Path "$mockSourcePath\Classes" -ItemType 'Directory' -Force + + # The class DSC resource in the built module. + $mockBuiltModuleScript = @' +class ResourceBase +{ + hidden [System.String] $NotADscProperty + + [DscProperty()] + [System.String] + $Ensure +} + +[DscResource()] +class MyDscResource : ResourceBase +{ + [MyDscResource] Get() + { + return [MyDscResource] $this + } + + [System.Boolean] Test() + { + return $true + } + + [void] Set() {} + + [DscProperty(Key)] + [System.String] $ProjectName + + [DscProperty()] + [System.String] $DescriptionTestProperty +} +'@ + # Uses Microsoft.PowerShell.Utility\Out-File to override the stub that is needed for the mocks. + $mockBuiltModuleScript | Microsoft.PowerShell.Utility\Out-File -FilePath "$mockBuiltModulePath\MyClassModule.psm1" -Encoding ascii -Force + + <# + The source file of class DSC resource. This file is not actually + referencing the base class to simplify the tests. + + The property DescriptionTestProperty is used to test description + parsing. + #> + $mockResourceSourceScript = @' +<# +.SYNOPSIS + Resource synopsis. + + .DESCRIPTION + Resource description. + + .PARAMETER ProjectName + ProjectName description. + + .PARAMETER DescriptionTestProperty + DescriptionTestProperty description. + + This is a second row with | various tests like double space and vertical bar. +#> +[DscResource()] +class MyDscResource +{ + [MyDscResource] Get() + { + return [MyDscResource] $this + } + + [System.Boolean] Test() + { + return $true + } + + [void] Set() {} + + [DscProperty(Key)] + [System.String] $ProjectName + + [DscProperty()] + [System.String] $DescriptionTestProperty +} +'@ + # Uses Microsoft.PowerShell.Utility\Out-File to override the stub that is needed for the mocks. + $mockResourceSourceScript | Microsoft.PowerShell.Utility\Out-File -FilePath "$mockSourcePath\Classes\010.MyDscResource.ps1" -Encoding ascii -Force + + $mockBaseClassSourceScript = @' +class ResourceBase +{ + hidden [System.String] $NotADscProperty + + [DscProperty()] + [System.String] + $Ensure +} +'@ + # Uses Microsoft.PowerShell.Utility\Out-File to override the stub that is needed for the mocks. + $mockBaseClassSourceScript | Microsoft.PowerShell.Utility\Out-File -FilePath "$mockSourcePath\Classes\001.ResourceBase.ps1" -Encoding ascii -Force + } + + It 'Should return the expected DSC class resource properties' { + $mockGetClassResourcePropertyParameters = @{ + SourcePath = $mockSourcePath + BuiltModuleScriptFilePath = Join-Path -Path $mockBuiltModulePath -ChildPath 'MyClassModule.psm1' + ClassName = @( + 'ResourceBase' + 'MyDscResource' + ) + } + + $getClassResourcePropertyResult = Get-ClassResourceProperty @mockGetClassResourcePropertyParameters + $getClassResourcePropertyResult | Should -HaveCount 3 + $getClassResourcePropertyResult.Name | Should -Contain 'Ensure' + $getClassResourcePropertyResult.Name | Should -Contain 'ProjectName' + + $ensurePropertyResult = $getClassResourcePropertyResult.Where({$_.Name -eq 'Ensure'}) + $ensurePropertyResult.State | Should -Be 'Write' + $ensurePropertyResult.Description | Should -BeNullOrEmpty + $ensurePropertyResult.DataType | Should -Be 'System.String' + $ensurePropertyResult.IsArray | Should -BeFalse + + $ensurePropertyResult = $getClassResourcePropertyResult.Where({$_.Name -eq 'ProjectName'}) + $ensurePropertyResult.State | Should -Be 'Key' + $ensurePropertyResult.Description | Should -Be 'ProjectName description.' + $ensurePropertyResult.DataType | Should -Be 'System.String' + $ensurePropertyResult.IsArray | Should -BeFalse + + $ensurePropertyResult = $getClassResourcePropertyResult.Where({$_.Name -eq 'DescriptionTestProperty'}) + $ensurePropertyResult.State | Should -Be 'Write' + $ensurePropertyResult.Description | Should -BeExactly @' +DescriptionTestProperty description. This is a second row with various tests like double space and vertical bar. +'@ + $ensurePropertyResult.DataType | Should -Be 'System.String' + $ensurePropertyResult.IsArray | Should -BeFalse + } + } +} diff --git a/tests/unit/public/New-DscResourceWikiPage.Tests.ps1 b/tests/unit/public/New-DscResourceWikiPage.Tests.ps1 index 0ae6497..bc776e1 100644 --- a/tests/unit/public/New-DscResourceWikiPage.Tests.ps1 +++ b/tests/unit/public/New-DscResourceWikiPage.Tests.ps1 @@ -958,7 +958,7 @@ Configuration Example } } - Context 'When the resource is describe with just one key property with not description for resource or property' { + Context 'When the resource is describe with just one key property with no description for resource or property' { BeforeAll { # The class DSC resource in the built module. $mockBuiltModuleScript = @' @@ -1440,6 +1440,306 @@ This is another row. -Exactly -Times 1 -Scope Context } } + + Context 'When the resource has a parent class that also has a DSC property' { + BeforeAll { + # The class DSC resource in the built module. + $mockBuiltModuleScript = @' +class ResourceBase +{ + hidden [System.String] $NotADscProperty + + [DscProperty()] + [System.String] + $Ensure +} + +[DscResource()] +class AzDevOpsProject : ResourceBase +{ + [AzDevOpsProject] Get() + { + return [AzDevOpsProject] $this + } + + [System.Boolean] Test() + { + return $true + } + + [void] Set() {} + + [DscProperty(Key)] + [System.String]$ProjectName + + [DscProperty()] + [System.String]$ProjectId + + [DscProperty()] + [ValidateSet('Up', 'Down')] + [System.String]$ValidateSetProperty + + [DscProperty(Mandatory)] + [System.String]$MandatoryProperty + + [DscProperty(NotConfigurable)] + [String[]]$Reasons +} +'@ + # Uses Microsoft.PowerShell.Utility\Out-File to override the stub that is needed for the mocks. + $mockBuiltModuleScript | Microsoft.PowerShell.Utility\Out-File -FilePath "$mockBuiltModulePath\MyClassModule.psm1" -Encoding ascii -Force + + # The source file of class DSC resource. + $mockResourceSourceScript = @' +<# + .SYNOPSIS + A DSC Resource for Azure DevOps that + represents the Project resource. + + This is another row. + + .DESCRIPTION + A DSC Resource for Azure DevOps that + represents the Project resource. + + This is another row. + + .PARAMETER ProjectName + ProjectName description. + + .PARAMETER ProjectId + ProjectId description. + + Second row with text. + + .PARAMETER MandatoryProperty + MandatoryProperty description. + + .PARAMETER Reasons + Reasons description. +#> +[DscResource()] +class AzDevOpsProject +{ + [AzDevOpsProject] Get() + { + return [AzDevOpsProject] $this + } + + [System.Boolean] Test() + { + return $true + } + + [void] Set() {} + + [DscProperty(Key)] + [System.String]$ProjectName + + [DscProperty()] + [System.String]$ProjectId + + [DscProperty()] + [ValidateSet('Up', 'Down')] + [System.String]$ValidateSetProperty + + [DscProperty(Mandatory)] + [System.String]$MandatoryProperty + + [DscProperty(NotConfigurable)] + [String[]]$Reasons +} +'@ + # Uses Microsoft.PowerShell.Utility\Out-File to override the stub that is needed for the mocks. + $mockResourceSourceScript | Microsoft.PowerShell.Utility\Out-File -FilePath "$mockSourcePath\Classes\010.AzDevOpsProject.ps1" -Encoding ascii -Force + + $mockBaseClassSourceScript = @' +<# + .SYNOPSIS + Synopsis for base class. + + .DESCRIPTION + Description for base class + + .PARAMETER Ensure + Ensure description. +#> +class ResourceBase +{ + hidden [System.String] $NotADscProperty + + [DscProperty()] + [System.String] + $Ensure +} +'@ + # Uses Microsoft.PowerShell.Utility\Out-File to override the stub that is needed for the mocks. + $mockBaseClassSourceScript | Microsoft.PowerShell.Utility\Out-File -FilePath "$mockSourcePath\Classes\001.ResourceBase.ps1" -Encoding ascii -Force + + $mockExpectedFileOutput = @' +# AzDevOpsProject + +## Parameters + +| Parameter | Attribute | DataType | Description | Allowed Values | +| --- | --- | --- | --- | --- | +| **Ensure** | Write | System.String | Ensure description. | | +| **ProjectName** | Key | System.String | ProjectName description. | | +| **ProjectId** | Write | System.String | ProjectId description. Second row with text. | | +| **ValidateSetProperty** | Write | System.String | | `Up`, `Down` | +| **MandatoryProperty** | Required | System.String | MandatoryProperty description. | | +| **Reasons** | Read | String[] | Reasons description. | | + +## Description + +A DSC Resource for Azure DevOps that +represents the Project resource. + +This is another row. +'@ -replace '\r?\n', "`r`n" + + $mockNewDscResourcePowerShellHelpParameters = @{ + SourcePath = $mockSourcePath + BuiltModulePath = $mockBuiltModulePath + OutputPath = $TestDrive + Verbose = $true + } + + Mock -CommandName Out-File + } + + It 'Should not throw an exception' { + { + New-DscResourceWikiPage @mockNewDscResourcePowerShellHelpParameters + } | Should -Not -Throw + } + + It 'Should produce the correct output' { + Assert-MockCalled ` + -CommandName Out-File ` + -ParameterFilter $script:outFileContent_ParameterFilter ` + -Exactly -Times 1 -Scope Context + } + } + + Context 'When the resource has a parent class that also have a DSC property, but the property does not have a parameter description' { + BeforeAll { + # The class DSC resource in the built module. + $mockBuiltModuleScript = @' +class ResourceBase +{ + [DscProperty()] + [System.String] + $Ensure +} + +[DscResource()] +class AzDevOpsProject : ResourceBase +{ + [AzDevOpsProject] Get() + { + return [AzDevOpsProject] $this + } + + [System.Boolean] Test() + { + return $true + } + + [void] Set() {} + + [DscProperty(Key)] + [System.String]$ProjectName +} +'@ + # Uses Microsoft.PowerShell.Utility\Out-File to override the stub that is needed for the mocks. + $mockBuiltModuleScript | Microsoft.PowerShell.Utility\Out-File -FilePath "$mockBuiltModulePath\MyClassModule.psm1" -Encoding ascii -Force + + # The source file of class DSC resource. + $mockResourceSourceScript = @' +<# + .SYNOPSIS + Resource synopsis. + + .DESCRIPTION + Resource description. + + .PARAMETER ProjectName + ProjectName description. +#> +[DscResource()] +class AzDevOpsProject +{ + [AzDevOpsProject] Get() + { + return [AzDevOpsProject] $this + } + + [System.Boolean] Test() + { + return $true + } + + [void] Set() {} + + [DscProperty(Key)] + [System.String]$ProjectName +} +'@ + # Uses Microsoft.PowerShell.Utility\Out-File to override the stub that is needed for the mocks. + $mockResourceSourceScript | Microsoft.PowerShell.Utility\Out-File -FilePath "$mockSourcePath\Classes\010.AzDevOpsProject.ps1" -Encoding ascii -Force + + $mockBaseClassSourceScript = @' +class ResourceBase +{ + hidden [System.String] $NotADscProperty + + [DscProperty()] + [System.String] + $Ensure +} +'@ + # Uses Microsoft.PowerShell.Utility\Out-File to override the stub that is needed for the mocks. + $mockBaseClassSourceScript | Microsoft.PowerShell.Utility\Out-File -FilePath "$mockSourcePath\Classes\001.ResourceBase.ps1" -Encoding ascii -Force + + $mockExpectedFileOutput = @' +# AzDevOpsProject + +## Parameters + +| Parameter | Attribute | DataType | Description | Allowed Values | +| --- | --- | --- | --- | --- | +| **Ensure** | Write | System.String | | | +| **ProjectName** | Key | System.String | ProjectName description. | | + +## Description + +Resource description. +'@ -replace '\r?\n', "`r`n" + + $mockNewDscResourcePowerShellHelpParameters = @{ + SourcePath = $mockSourcePath + BuiltModulePath = $mockBuiltModulePath + OutputPath = $TestDrive + Verbose = $true + } + + Mock -CommandName Out-File + } + + It 'Should not throw an exception' { + { + New-DscResourceWikiPage @mockNewDscResourcePowerShellHelpParameters + } | Should -Not -Throw + } + + It 'Should produce the correct output' { + Assert-MockCalled ` + -CommandName Out-File ` + -ParameterFilter $script:outFileContent_ParameterFilter ` + -Exactly -Times 1 -Scope Context + } + } } } }