diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 1900521..fb38f1f 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -16,10 +16,10 @@ #### This Pull Request (PR) fixes the following issues #### Task list diff --git a/.vscode/settings.json b/.vscode/settings.json index f831d3a..e74f889 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -25,6 +25,8 @@ "powershell" ], "cSpell.words": [ + "Hashtable", + "notin" ], "cSpell.ignorePaths": [ ".git" diff --git a/CHANGELOG.md b/CHANGELOG.md index e310bdc..5d84feb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,10 +5,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- DscResource.Base + - A new private function `ConvertFrom-Reason` was added which takes an + array of `[Reason]` and coverts it to an array of `[System.Collections.Hashtable]`. + ### Changed - DscResource.Base - Enable Pester's new code coverage method. + - The private function `ConvertTo-Reason` was renamed `Resolve-Reason`. +- `ResourceBase` + - The property `Reasons` in derived class-based resources is now expected + to use the type `[System.Collections.Hashtable[]]` ([issue #4](https://github.com/dsccommunity/DscResource.Base/issues/4)). ### Fixed diff --git a/source/Classes/010.ResourceBase.ps1 b/source/Classes/010.ResourceBase.ps1 index 54231f4..137d0c1 100644 --- a/source/Classes/010.ResourceBase.ps1 +++ b/source/Classes/010.ResourceBase.ps1 @@ -122,7 +122,8 @@ class ResourceBase { # Always return an empty array if all properties are in desired state. $dscResourceObject.Reasons = $propertiesNotInDesiredState | - ConvertTo-Reason -ResourceName $this.GetType().Name + Resolve-Reason -ResourceName $this.GetType().Name | + ConvertFrom-Reason } # Return properties. diff --git a/source/Private/ConvertFrom-Reason.ps1 b/source/Private/ConvertFrom-Reason.ps1 new file mode 100644 index 0000000..df8abe0 --- /dev/null +++ b/source/Private/ConvertFrom-Reason.ps1 @@ -0,0 +1,58 @@ +<# + .SYNOPSIS + Returns a array of the type `System.Collections.Hashtable`. + + .DESCRIPTION + This command converts an array of [Reason] that is returned by the command + `Resolve-Reason`. The result is an array of the type `[System.Collections.Hashtable]` + that can be returned as the value of a DSC resource's property **Reasons**. + + .PARAMETER Reason + Specifies an array of `[Reason]`. Normally the result from the command `Resolve-Reason`. + + .EXAMPLE + Resolve-Reason -Reason (Resolve-Reason) -ResourceName 'MyResource' + + Returns an array of `[System.Collections.Hashtable]` with the converted + `[Reason[]]`. + + .OUTPUTS + [System.Collections.Hashtable[]] +#> +function ConvertFrom-Reason +{ + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseOutputTypeCorrectly', '', Justification = 'Because the rule does not understands that the command returns [System.Collections.Hashtable[]] when using , (comma) in the return statement')] + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('UseSyntacticallyCorrectExamples', '', Justification = 'Because the rule does not yet support parsing the code when the output type is not available. The ScriptAnalyzer rule UseSyntacticallyCorrectExamples will always error in the editor due to https://github.com/indented-automation/Indented.ScriptAnalyzerRules/issues/8.')] + [CmdletBinding()] + [OutputType([System.Collections.Hashtable[]])] + param + ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [AllowEmptyCollection()] + [AllowNull()] + [Reason[]] + $Reason + ) + + begin + { + # Always return an empty array if there are nothing to convert. + $reasonsAsHashtable = [System.Collections.Hashtable[]] @() + } + + process + { + foreach ($currentReason in $Reason) + { + $reasonsAsHashtable += [System.Collections.Hashtable] @{ + Code = $currentReason.Code + Phrase = $currentReason.Phrase + } + } + } + + end + { + return , [System.Collections.Hashtable[]] $reasonsAsHashtable + } +} diff --git a/source/Private/ConvertTo-Reason.ps1 b/source/Private/Resolve-Reason.ps1 similarity index 93% rename from source/Private/ConvertTo-Reason.ps1 rename to source/Private/Resolve-Reason.ps1 index 6586273..15ff5e2 100644 --- a/source/Private/ConvertTo-Reason.ps1 +++ b/source/Private/Resolve-Reason.ps1 @@ -3,9 +3,8 @@ Returns a array of the type `[Reason]`. .DESCRIPTION - This command converts the array of properties that is returned by the command - `Compare-DscParameterState`. The result is an array of the type `[Reason]` that - can be returned in a DSC resource's property **Reasons**. + This command builds an array from the properties that is returned by the command + `Compare-DscParameterState`. The result is an array of the type `[Reason]`. .PARAMETER Property The result from the command Compare-DscParameterState. @@ -15,7 +14,7 @@ the correct value. .EXAMPLE - ConvertTo-Reason -Property (Compare-DscParameterState) -ResourceName 'MyResource' + Resolve-Reason -Property (Compare-DscParameterState) -ResourceName 'MyResource' Returns an array of `[Reason]` that contain all the properties not in desired state and why a specific property is not in desired state. @@ -23,7 +22,7 @@ .OUTPUTS [Reason[]] #> -function ConvertTo-Reason +function Resolve-Reason { [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('UseSyntacticallyCorrectExamples', '', Justification = 'Because the rule does not yet support parsing the code when the output type is not available. The ScriptAnalyzer rule UseSyntacticallyCorrectExamples will always error in the editor due to https://github.com/indented-automation/Indented.ScriptAnalyzerRules/issues/8.')] [CmdletBinding()] diff --git a/tests/Unit/Classes/ResourceBase.Tests.ps1 b/tests/Unit/Classes/ResourceBase.Tests.ps1 index c2fec84..6bc8acc 100644 --- a/tests/Unit/Classes/ResourceBase.Tests.ps1 +++ b/tests/Unit/Classes/ResourceBase.Tests.ps1 @@ -162,7 +162,7 @@ class MyMockResource : ResourceBase $MyResourceProperty2 [DscProperty(NotConfigurable)] - [Reason[]] + [System.Collections.Hashtable[]] $Reasons <# @@ -213,6 +213,8 @@ $script:mockResourceBaseInstance = [MyMockResource]::new() $getResult.MyResourceKeyProperty1 | Should -Be 'MyValue1' $getResult.MyResourceProperty2 | Should -Be 'MyValue2' $getResult.Ensure | Should -Be ([Ensure]::Present) + + Should -ActualValue $getResult.Reasons -HaveType [System.Collections.Hashtable[]] $getResult.Reasons | Should -BeNullOrEmpty } } @@ -248,7 +250,7 @@ class MyMockResource : ResourceBase $MyResourceProperty2 [DscProperty(NotConfigurable)] - [Reason[]] + [System.Collections.Hashtable[]] $Reasons <# @@ -292,6 +294,8 @@ $script:mockResourceBaseInstance = [MyMockResource]::new() $getResult.MyResourceKeyProperty1 | Should -Be 'MyValue1' $getResult.MyResourceProperty2 | Should -BeNullOrEmpty $getResult.Ensure | Should -Be ([Ensure]::Absent) + + Should -ActualValue $getResult.Reasons -HaveType [System.Collections.Hashtable[]] $getResult.Reasons | Should -BeNullOrEmpty } } @@ -328,7 +332,7 @@ class MyMockResource : ResourceBase $MyResourceProperty2 [DscProperty(NotConfigurable)] - [Reason[]] + [System.Collections.Hashtable[]] $Reasons [System.Collections.Hashtable] GetCurrentState([System.Collections.Hashtable] $properties) @@ -364,6 +368,8 @@ $script:mockResourceBaseInstance = [MyMockResource]::new() $getResult.MyResourceKeyProperty1 | Should -Be 'MyValue1' $getResult.MyResourceProperty2 | Should -Be 'MyValue2' $getResult.Ensure | Should -Be ([Ensure]::Present) + + Should -ActualValue $getResult.Reasons -HaveType [System.Collections.Hashtable[]] $getResult.Reasons | Should -BeNullOrEmpty } } @@ -399,7 +405,7 @@ class MyMockResource : ResourceBase $MyResourceProperty2 [DscProperty(NotConfigurable)] - [Reason[]] + [System.Collections.Hashtable[]] $Reasons [System.Collections.Hashtable] GetCurrentState([System.Collections.Hashtable] $properties) @@ -435,6 +441,8 @@ $script:mockResourceBaseInstance = [MyMockResource]::new() $getResult.MyResourceKeyProperty1 | Should -Be 'MyValue1' $getResult.MyResourceProperty2 | Should -BeNullOrEmpty $getResult.Ensure | Should -Be ([Ensure]::Absent) + + Should -ActualValue $getResult.Reasons -HaveType [System.Collections.Hashtable[]] $getResult.Reasons | Should -BeNullOrEmpty } } @@ -476,7 +484,7 @@ class MyMockResource : ResourceBase $MyResourceProperty2 [DscProperty(NotConfigurable)] - [Reason[]] + [System.Collections.Hashtable[]] $Reasons MyMockResource() : base () @@ -518,6 +526,8 @@ $script:mockResourceBaseInstance = [MyMockResource]::new() $getResult.MyResourceProperty2 | Should -Be 'MyValue2' $getResult.Ensure | Should -Be ([Ensure]::Present) + Should -ActualValue $getResult.Reasons -HaveType [System.Collections.Hashtable[]] + $getResult.Reasons | Should -HaveCount 1 $getResult.Reasons[0].Code | Should -Be 'MyMockResource:MyMockResource:MyResourceProperty2' $getResult.Reasons[0].Phrase | Should -Be 'The property MyResourceProperty2 should be "NewValue2", but was "MyValue2"' @@ -550,7 +560,7 @@ class MyMockResource : ResourceBase $MyResourceProperty2 [DscProperty(NotConfigurable)] - [Reason[]] + [System.Collections.Hashtable[]] $Reasons MyMockResource() : base () @@ -588,6 +598,8 @@ $script:mockResourceBaseInstance = [MyMockResource]::new() $getResult.MyResourceKeyProperty1 | Should -Be 'MyValue1' $getResult.Ensure | Should -Be ([Ensure]::Absent) + Should -ActualValue $getResult.Reasons -HaveType [System.Collections.Hashtable[]] + $getResult.Reasons | Should -HaveCount 1 $getResult.Reasons[0].Code | Should -Be 'MyMockResource:MyMockResource:Ensure' $getResult.Reasons[0].Phrase | Should -Be 'The property Ensure should be "Present", but was "Absent"' @@ -626,7 +638,7 @@ class MyMockResource : ResourceBase $MyResourceProperty2 [DscProperty(NotConfigurable)] - [Reason[]] + [System.Collections.Hashtable[]] $Reasons MyMockResource() : base () @@ -668,6 +680,8 @@ $script:mockResourceBaseInstance = [MyMockResource]::new() $getResult.MyResourceProperty2 | Should -Be 'MyValue2' $getResult.Ensure | Should -Be ([Ensure]::Present) + Should -ActualValue $getResult.Reasons -HaveType [System.Collections.Hashtable[]] + $getResult.Reasons | Should -HaveCount 1 $getResult.Reasons[0].Code | Should -Be 'MyMockResource:MyMockResource:Ensure' $getResult.Reasons[0].Phrase | Should -Be 'The property Ensure should be "Absent", but was "Present"' @@ -701,7 +715,7 @@ class MyMockResource : ResourceBase $MyResourceProperty2 [DscProperty(NotConfigurable)] - [Reason[]] + [System.Collections.Hashtable[]] $Reasons [System.Collections.Hashtable] GetCurrentState([System.Collections.Hashtable] $properties) @@ -738,6 +752,8 @@ $script:mockResourceBaseInstance = [MyMockResource]::new() $getResult.MyResourceProperty2 | Should -Be 'MyValue2' $getResult.Ensure | Should -Be ([Ensure]::Absent) + Should -ActualValue $getResult.Reasons -HaveType [System.Collections.Hashtable[]] + $getResult.Reasons | Should -HaveCount 2 # The order in the array was sometimes different so could not use array index ($getResult.Reasons[0]). @@ -779,7 +795,7 @@ class MyMockResource : ResourceBase $MyResourceProperty2 [DscProperty(NotConfigurable)] - [Reason[]] + [System.Collections.Hashtable[]] $Reasons [System.Collections.Hashtable] GetCurrentState([System.Collections.Hashtable] $properties) @@ -816,6 +832,8 @@ $script:mockResourceBaseInstance = [MyMockResource]::new() $getResult.MyResourceProperty2 | Should -Be 'MyValue2' $getResult.Ensure | Should -Be ([Ensure]::Present) + Should -ActualValue $getResult.Reasons -HaveType [System.Collections.Hashtable[]] + $getResult.Reasons | Should -HaveCount 1 $getResult.Reasons[0].Code | Should -Be 'MyMockResource:MyMockResource:Ensure' diff --git a/tests/Unit/Private/ConvertFrom-Reason.Tests.ps1 b/tests/Unit/Private/ConvertFrom-Reason.Tests.ps1 new file mode 100644 index 0000000..e6048c0 --- /dev/null +++ b/tests/Unit/Private/ConvertFrom-Reason.Tests.ps1 @@ -0,0 +1,125 @@ +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies has been resolved, so if this module is not available, run 'noop' task. + if (-not (Get-Module -Name 'DscResource.Test' -ListAvailable)) + { + # Redirect all streams to $null, except the error stream (stream 2) + & "$PSScriptRoot/../../build.ps1" -Tasks 'noop' 2>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies has not been resolved, this will throw an error. + Import-Module -Name 'DscResource.Test' -Force -ErrorAction 'Stop' + } + } + catch [System.IO.FileNotFoundException] + { + throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -ResolveDependency -Tasks build" first.' + } +} + +BeforeAll { + $script:dscModuleName = 'DscResource.Base' + + Import-Module -Name $script:dscModuleName + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:dscModuleName +} + +AfterAll { + $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') + $PSDefaultParameterValues.Remove('Mock:ModuleName') + $PSDefaultParameterValues.Remove('Should:ModuleName') + + # Unload the module being tested so that it doesn't impact any other tests. + Get-Module -Name $script:dscModuleName -All | Remove-Module -Force +} + +Describe 'ConvertFrom-Reason' -Tag 'Private' { + Context 'When passing an empty collection' { + It 'Should return an empty collection' { + InModuleScope -ScriptBlock { + $result = ConvertFrom-Reason -Reason @() + + $result | Should -HaveCount 0 + } + } + } + + Context 'When passing a null value' { + It 'Should return an empty collection' { + InModuleScope -ScriptBlock { + $result = ConvertFrom-Reason -Reason $null + + $result | Should -HaveCount 0 + } + } + } + + Context 'When passing as named parameter' { + It 'Should return the correct values in a hashtable' { + InModuleScope -ScriptBlock { + $firstReason = [Reason] @{ + Code = 'MyResource:MyResource:MyResourceProperty1' + Phrase = 'The property MyResourceProperty1 should be "MyNewValue1", but was "MyValue1"' + } + + $secondReason = [Reason] @{ + Code = 'MyResource:MyResource:MyResourceProperty2' + Phrase = 'The property MyResourceProperty2 should be ["MyNewValue2","MyNewValue3"], but was ["MyValue2","MyValue3"]' + } + + $mockReason = [Reason[]] @($firstReason, $secondReason) + + $result = ConvertFrom-Reason -Reason $mockReason + + Should -ActualValue $result -HaveType [System.Collections.Hashtable[]] + + $result | Should -HaveCount 2 + + $result.Code | Should -Contain 'MyResource:MyResource:MyResourceProperty1' + $result.Phrase | Should -Contain 'The property MyResourceProperty1 should be "MyNewValue1", but was "MyValue1"' + + $result.Code | Should -Contain 'MyResource:MyResource:MyResourceProperty2' + $result.Phrase | Should -Contain 'The property MyResourceProperty2 should be ["MyNewValue2","MyNewValue3"], but was ["MyValue2","MyValue3"]' + } + } + } + + Context 'When passing in the pipeline' { + It 'Should return the correct values in a hashtable' { + InModuleScope -ScriptBlock { + $firstReason = [Reason] @{ + Code = 'MyResource:MyResource:MyResourceProperty1' + Phrase = 'The property MyResourceProperty1 should be "MyNewValue1", but was "MyValue1"' + } + + $secondReason = [Reason] @{ + Code = 'MyResource:MyResource:MyResourceProperty2' + Phrase = 'The property MyResourceProperty2 should be ["MyNewValue2","MyNewValue3"], but was ["MyValue2","MyValue3"]' + } + + $mockReason = [Reason[]] @($firstReason, $secondReason) + + $result = $mockReason | ConvertFrom-Reason + + Should -ActualValue $result -HaveType [System.Collections.Hashtable[]] + + $result | Should -HaveCount 2 + + $result.Code | Should -Contain 'MyResource:MyResource:MyResourceProperty1' + $result.Phrase | Should -Contain 'The property MyResourceProperty1 should be "MyNewValue1", but was "MyValue1"' + + $result.Code | Should -Contain 'MyResource:MyResource:MyResourceProperty2' + $result.Phrase | Should -Contain 'The property MyResourceProperty2 should be ["MyNewValue2","MyNewValue3"], but was ["MyValue2","MyValue3"]' + } + } + } +} diff --git a/tests/Unit/Private/ConvertTo-Reason.Tests.ps1 b/tests/Unit/Private/Resolve-Reason.Tests.ps1 similarity index 90% rename from tests/Unit/Private/ConvertTo-Reason.Tests.ps1 rename to tests/Unit/Private/Resolve-Reason.Tests.ps1 index 289e289..0c88bb1 100644 --- a/tests/Unit/Private/ConvertTo-Reason.Tests.ps1 +++ b/tests/Unit/Private/Resolve-Reason.Tests.ps1 @@ -42,13 +42,11 @@ AfterAll { Get-Module -Name $script:dscModuleName -All | Remove-Module -Force } -Describe 'ConvertTo-Reason' -Tag 'Private' { +Describe 'Resolve-Reason' -Tag 'Private' { Context 'When passing an empty collection' { It 'Should return an empty collection' { InModuleScope -ScriptBlock { - $mockProperties = @() - - $result = ConvertTo-Reason -Property $mockProperties -ResourceName 'MyResource' + $result = Resolve-Reason -Property @() -ResourceName 'MyResource' $result | Should -HaveCount 0 } @@ -58,9 +56,7 @@ Describe 'ConvertTo-Reason' -Tag 'Private' { Context 'When passing a null value' { It 'Should return an empty collection' { InModuleScope -ScriptBlock { - $mockProperties = @() - - $result = ConvertTo-Reason -Property $null -ResourceName 'MyResource' + $result = Resolve-Reason -Property $null -ResourceName 'MyResource' $result | Should -HaveCount 0 } @@ -83,7 +79,7 @@ Describe 'ConvertTo-Reason' -Tag 'Private' { } ) - $result = ConvertTo-Reason -Property $mockProperties -ResourceName 'MyResource' + $result = Resolve-Reason -Property $mockProperties -ResourceName 'MyResource' $result | Should -HaveCount 2 @@ -112,7 +108,7 @@ Describe 'ConvertTo-Reason' -Tag 'Private' { } ) - $result = $mockProperties | ConvertTo-Reason -ResourceName 'MyResource' + $result = $mockProperties | Resolve-Reason -ResourceName 'MyResource' $result | Should -HaveCount 2 @@ -146,7 +142,7 @@ Describe 'ConvertTo-Reason' -Tag 'Private' { } ) - $result = ConvertTo-Reason -Property $mockProperties -ResourceName 'MyResource' + $result = Resolve-Reason -Property $mockProperties -ResourceName 'MyResource' $result | Should -HaveCount 1 @@ -179,7 +175,7 @@ Describe 'ConvertTo-Reason' -Tag 'Private' { } ) - $result = ConvertTo-Reason -Property $mockProperties -ResourceName 'MyResource' + $result = Resolve-Reason -Property $mockProperties -ResourceName 'MyResource' $result | Should -HaveCount 1 @@ -201,7 +197,7 @@ Describe 'ConvertTo-Reason' -Tag 'Private' { } ) - $result = ConvertTo-Reason -Property $mockProperties -ResourceName 'MyResource' + $result = Resolve-Reason -Property $mockProperties -ResourceName 'MyResource' $result | Should -HaveCount 1