diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml new file mode 100644 index 0000000..841d3f5 --- /dev/null +++ b/.github/workflows/CI.yml @@ -0,0 +1,45 @@ +name: CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + schedule: + - cron: "0 0 * * *" + +jobs: + powershell-core: + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v2 + + - name: Restore + run: tools/restore.ps1 + shell: pwsh + + - name: test + run: tools/run-tests.ps1 + shell: pwsh + + windows-powershell: + runs-on: windows-latest + + steps: + - uses: actions/checkout@v2 + + - name: Restore + run: tools/restore.ps1 + shell: powershell + + - name: test + run: tools/run-tests.ps1 + shell: powershell + + - name: Test manifest + run: Test-ModuleManifest -Path ./FeatureFlags.psd1 + shell: powershell \ No newline at end of file diff --git a/README.md b/README.md index 4ddd59a..257f1e1 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,3 @@ -[![Build Status](https://dev.azure.com/PowerShell-FeatureFlags/PowerShell-FeatureFlags/_apis/build/status/microsoft.PowerShell-FeatureFlags?branchName=master)](https://dev.azure.com/PowerShell-FeatureFlags/PowerShell-FeatureFlags/_build/latest?definitionId=1&branchName=master) [![Nuget](https://img.shields.io/nuget/v/FeatureFlags.PowerShell)](https://www.nuget.org/packages/FeatureFlags.PowerShell/1.0.0) [![Platforms](https://img.shields.io/powershellgallery/p/FeatureFlags.svg)](https://www.powershellgallery.com/packages/FeatureFlags/) [![FeatureFlags](https://img.shields.io/powershellgallery/v/FeatureFlags.svg)](https://www.powershellgallery.com/packages/FeatureFlags/) diff --git a/azure-pipelines.yml b/azure-pipelines.yml deleted file mode 100644 index af2f3f9..0000000 --- a/azure-pipelines.yml +++ /dev/null @@ -1,74 +0,0 @@ -trigger: - branches: - include: - - master - paths: - exclude: - - README.md - - examples/* - - .gitignore - -schedules: -- cron: "0 0 * * *" - displayName: Daily CI build - always: true - branches: - include: - - master - -jobs: -- job: PowerShellCore - strategy: - matrix: - linux: - imageName: 'ubuntu-18.04' - mac: - imageName: 'macos-10.15' - windows: - imageName: 'windows-2019' - - pool: - vmImage: $(imageName) - - steps: - - task: PowerShell@2 - displayName: Restore - inputs: - filePath: 'tools/restore.ps1' - pwsh: true - - - task: PowerShell@2 - displayName: Test - inputs: - filePath: 'tools/run-tests.ps1' - pwsh: true - - - task: PublishTestResults@2 - inputs: - testResultsFormat: "NUnit" - testResultsFiles: "test/results.xml" - -- job: WindowsPowershell - pool: - vmImage: 'vs2017-win2016' - steps: - - task: PowerShell@2 - displayName: Restore - inputs: - filePath: 'tools/restore.ps1' - - - task: PowerShell@2 - displayName: Test - inputs: - filePath: 'tools/run-tests.ps1' - - - task: PublishTestResults@2 - inputs: - testResultsFormat: "NUnit" - testResultsFiles: "test/results.xml" - - - task: PowerShell@2 - displayName: TestManifest - inputs: - targetType: inline - script: 'Test-ModuleManifest -Path ./FeatureFlags.psd1' \ No newline at end of file diff --git a/test/FeatureFlags.Tests.ps1 b/test/FeatureFlags.Tests.ps1 index 3ec8676..3d82a29 100644 --- a/test/FeatureFlags.Tests.ps1 +++ b/test/FeatureFlags.Tests.ps1 @@ -4,18 +4,20 @@ https://github.com/pester/Pester/wiki/Installation-and-Update. After updating, run the Invoke-Pester cmdlet from the project directory. #> -$ModuleName = "FeatureFlags" -Get-Module $ModuleName | Remove-Module -Force -Import-Module $PSScriptRoot\test-functions.psm1 +BeforeAll { + $ModuleName = "FeatureFlags" + Get-Module $ModuleName | Remove-Module -Force + Import-Module $PSScriptRoot\test-functions.psm1 -$VerbosePreference = "Continue" -$Module = Import-Module $PSScriptRoot\..\${ModuleName}.psd1 -Force -PassThru -if ($null -eq $Module) { - Write-Error "Could not import $ModuleName" - exit 1 + $VerbosePreference = "Continue" + $Module = Import-Module $PSScriptRoot\..\${ModuleName}.psd1 -Force -PassThru + if ($null -eq $Module) { + Write-Error "Could not import $ModuleName" + exit 1 + } + Write-Host "Done." + Write-Host $Module.ExportedCommands.Values.Name } -Write-Host "Done." -Write-Host $Module.ExportedCommands.Values.Name Describe 'Confirm-FeatureFlagConfig' { Context 'Validation of invalid configuration' { @@ -270,22 +272,24 @@ Describe 'Get-FeatureFlagConfigFromFile' { Describe 'Test-FeatureFlag' { Context 'allowlist condition' { Context 'Simple allowlist configuration' { - $serializedConfig = @" - { - "stages": { - "all": [ - {"allowlist": [".*"]} - ] - }, - "features": { - "well-tested": { - "stages": ["all"] + BeforeAll { + $serializedConfig = @" + { + "stages": { + "all": [ + {"allowlist": [".*"]} + ] + }, + "features": { + "well-tested": { + "stages": ["all"] + } } } - } "@ - Confirm-FeatureFlagConfig $serializedConfig - $config = ConvertFrom-Json $serializedConfig + Confirm-FeatureFlagConfig $serializedConfig + $config = ConvertFrom-Json $serializedConfig + } It 'Rejects non-existing features' { Test-FeatureFlag "feature1" "Storage/master" $config | Should -Be $false @@ -297,25 +301,27 @@ Describe 'Test-FeatureFlag' { } Context 'Chained allowlist configuration' { - $serializedConfig = @" - { - "stages": { - "test-repo-and-branch": [ - {"allowlist": [ - "storage1/.*", - "storage2/dev-branch" - ]} - ] - }, - "features": { - "experimental-feature": { - "stages": ["test-repo-and-branch"] + BeforeAll { + $serializedConfig = @" + { + "stages": { + "test-repo-and-branch": [ + {"allowlist": [ + "storage1/.*", + "storage2/dev-branch" + ]} + ] + }, + "features": { + "experimental-feature": { + "stages": ["test-repo-and-branch"] + } } } - } "@ - Confirm-FeatureFlagConfig $serializedConfig - $config = ConvertFrom-Json $serializedConfig + Confirm-FeatureFlagConfig $serializedConfig + $config = ConvertFrom-Json $serializedConfig + } It 'Returns true if the regex matches' { Test-FeatureFlag "experimental-feature" "storage1/master" $config | Should -Be $true @@ -330,22 +336,24 @@ Describe 'Test-FeatureFlag' { Context 'denylist condition' { Context 'Reject-all configuration' { - $serializedConfig = @" - { - "stages": { - "none": [ - {"denylist": [".*"]} - ] - }, - "features": { - "disabled": { - "stages": ["none"] + BeforeAll { + $serializedConfig = @" + { + "stages": { + "none": [ + {"denylist": [".*"]} + ] + }, + "features": { + "disabled": { + "stages": ["none"] + } } } - } "@ - Confirm-FeatureFlagConfig $serializedConfig - $config = ConvertFrom-Json $serializedConfig + Confirm-FeatureFlagConfig $serializedConfig + $config = ConvertFrom-Json $serializedConfig + } It 'Rejects everything' { Test-FeatureFlag "disabled" "Storage/master" $config | Should -Be $false @@ -355,23 +363,25 @@ Describe 'Test-FeatureFlag' { } Context 'Reject single-value configuration' { - $serializedConfig = @" - { - "stages": { - "all-except-important": [ - {"denylist": ["^important$"]} - ] - }, - "features": { - "some-feature": - { - "stages": ["all-except-important"] + BeforeAll { + $serializedConfig = @" + { + "stages": { + "all-except-important": [ + {"denylist": ["^important$"]} + ] + }, + "features": { + "some-feature": + { + "stages": ["all-except-important"] + } } } - } "@ - Confirm-FeatureFlagConfig $serializedConfig - $config = ConvertFrom-Json $serializedConfig + Confirm-FeatureFlagConfig $serializedConfig + $config = ConvertFrom-Json $serializedConfig + } # Given that the regex is ^important$, only the exact string "important" will match the denylist. It 'Allows the flag if the predicate does not match exactly' { @@ -387,22 +397,24 @@ Describe 'Test-FeatureFlag' { } Context 'Reject multiple-value configuration' { - $serializedConfig = @" - { - "stages": { - "all-except-important": [ - {"denylist": ["storage-important/master", "storage-important2/master"]} - ] - }, - "features": { - "some-feature": { - "stages": ["all-except-important"] + BeforeAll { + $serializedConfig = @" + { + "stages": { + "all-except-important": [ + {"denylist": ["storage-important/master", "storage-important2/master"]} + ] + }, + "features": { + "some-feature": { + "stages": ["all-except-important"] + } } } - } "@ - Confirm-FeatureFlagConfig $serializedConfig - $config = ConvertFrom-Json $serializedConfig + Confirm-FeatureFlagConfig $serializedConfig + $config = ConvertFrom-Json $serializedConfig + } It 'Allows predicates not matching the denylist' { Test-FeatureFlag "some-feature" "storage1/master" $config | Should -Be $true @@ -418,23 +430,25 @@ Describe 'Test-FeatureFlag' { } Context 'Mixed allowlist/denylist configuration' { - $serializedConfig = @" - { - "stages": { - "all-storage-important": [ - {"allowlist": ["storage.*"]}, - {"denylist": ["storage-important/master", "storage-important2/master"]} - ] - }, - "features": { - "some-feature": { - "stages": ["all-storage-important"] + BeforeAll { + $serializedConfig = @" + { + "stages": { + "all-storage-important": [ + {"allowlist": ["storage.*"]}, + {"denylist": ["storage-important/master", "storage-important2/master"]} + ] + }, + "features": { + "some-feature": { + "stages": ["all-storage-important"] + } } } - } "@ - Confirm-FeatureFlagConfig $serializedConfig - $config = ConvertFrom-Json $serializedConfig + Confirm-FeatureFlagConfig $serializedConfig + $config = ConvertFrom-Json $serializedConfig + } It 'Rejects storage important / important2 master branches' { Test-FeatureFlag "some-feature" "storage-important/master" $config | Should -Be $false @@ -454,34 +468,36 @@ Describe 'Test-FeatureFlag' { } Context 'Probability condition' { - $serializedConfig = @" - { - "stages": { - "all": [ - {"probability": 1} - ], - "none": [ - {"probability": 0} - ], - "10percent": [ - {"probability": 0.1} - ] - }, - "features": { - "well-tested": { - "stages": ["all"] + BeforeAll { + $serializedConfig = @" + { + "stages": { + "all": [ + {"probability": 1} + ], + "none": [ + {"probability": 0} + ], + "10percent": [ + {"probability": 0.1} + ] }, - "not-launched": { - "stages": ["none"] - }, - "10pc-feature": { - "stages": ["10percent"] + "features": { + "well-tested": { + "stages": ["all"] + }, + "not-launched": { + "stages": ["none"] + }, + "10pc-feature": { + "stages": ["10percent"] + } } } - } "@ - Confirm-FeatureFlagConfig $serializedConfig - $config = ConvertFrom-Json $serializedConfig + Confirm-FeatureFlagConfig $serializedConfig + $config = ConvertFrom-Json $serializedConfig + } It 'Always allows with probability 1' { Test-FeatureFlag "well-tested" "storage-important/master" $config | Should -Be $true @@ -523,25 +539,27 @@ Describe 'Test-FeatureFlag' { } Context 'Complex allowlist + denylist + probability configuration' { - $serializedConfig = @" - { - "stages": { - "all-storage-important-50pc": [ - {"allowlist": ["storage.*"]}, - {"denylist": ["storage-important/master", "storage-important2/master"]}, - {"probability": 0.5} - ] - }, - "features": { - "some-feature": { - "stages": ["all-storage-important-50pc"] + BeforeAll { + $serializedConfig = @" + { + "stages": { + "all-storage-important-50pc": [ + {"allowlist": ["storage.*"]}, + {"denylist": ["storage-important/master", "storage-important2/master"]}, + {"probability": 0.5} + ] + }, + "features": { + "some-feature": { + "stages": ["all-storage-important-50pc"] + } } } - } "@ - Confirm-FeatureFlagConfig $serializedConfig - $config = ConvertFrom-Json $serializedConfig + Confirm-FeatureFlagConfig $serializedConfig + $config = ConvertFrom-Json $serializedConfig + } It 'Rejects storage important / important2 master branches' { Test-FeatureFlag "some-feature" "storage-important/master" $config | Should -Be $false @@ -571,10 +589,12 @@ Describe 'Test-FeatureFlag' { Describe 'Get-EvaluatedFeatureFlags' -Tag Features { Context 'Verify evaluation of all feature flags' { - $serializedConfig = Get-Content -Raw "$PSScriptRoot\multiple-stages-features.json" - Confirm-FeatureFlagConfig $serializedConfig - $config = ConvertFrom-Json $serializedConfig; - Mock New-Item -ModuleName FeatureFlags {} + BeforeAll { + $serializedConfig = Get-Content -Raw "$PSScriptRoot\multiple-stages-features.json" + Confirm-FeatureFlagConfig $serializedConfig + $config = ConvertFrom-Json $serializedConfig; + Mock New-Item -ModuleName FeatureFlags {} + } It 'Returns expected feature flags' { $expected = @{ "filetracker"=$true; "newestfeature"=$true; "testfeature"=$false } @@ -587,20 +607,22 @@ Describe 'Get-EvaluatedFeatureFlags' -Tag Features { Describe 'Out-EvaluatedFeaturesFiles' -Tag Features { Context 'Verify output file content' { - $global:featuresJsonContent = New-Object 'System.Collections.ArrayList()' - $global:featuresIniContent = New-Object 'System.Collections.ArrayList()' - $global:featuresEnvConfigContent = New-Object 'System.Collections.ArrayList()' + BeforeAll { + $global:featuresJsonContent = New-Object 'System.Collections.ArrayList()' + $global:featuresIniContent = New-Object 'System.Collections.ArrayList()' + $global:featuresEnvConfigContent = New-Object 'System.Collections.ArrayList()' - $serializedConfig = Get-Content -Raw "$PSScriptRoot\multiple-stages-features.json" - Confirm-FeatureFlagConfig $serializedConfig - $config = ConvertFrom-Json $serializedConfig + $serializedConfig = Get-Content -Raw "$PSScriptRoot\multiple-stages-features.json" + Confirm-FeatureFlagConfig $serializedConfig + $config = ConvertFrom-Json $serializedConfig - Mock -ModuleName FeatureFlags New-Item {} - Mock -ModuleName $ModuleName Test-Path { Write-Output $true } - Mock -ModuleName $ModuleName Remove-Item {} - Mock -ModuleName $ModuleName Out-File { ${global:featuresJsonContent}.Add($InputObject) } -ParameterFilter { $FilePath.EndsWith("features.json") } - Mock -ModuleName $ModuleName Add-Content { ${global:featuresIniContent}.Add($Value) } -ParameterFilter { $Path.EndsWith("features.ini") } - Mock -ModuleName $ModuleName Add-Content { ${global:featuresEnvConfigContent}.Add($Value) } -ParameterFilter { $Path.EndsWith("features.env.config") } + Mock -ModuleName FeatureFlags New-Item {} + Mock -ModuleName $ModuleName Test-Path { Write-Output $true } + Mock -ModuleName $ModuleName Remove-Item {} + Mock -ModuleName $ModuleName Out-File { ${global:featuresJsonContent}.Add($InputObject) } -ParameterFilter { $FilePath.EndsWith("features.json") } + Mock -ModuleName $ModuleName Add-Content { ${global:featuresIniContent}.Add($Value) } -ParameterFilter { $Path.EndsWith("features.ini") } + Mock -ModuleName $ModuleName Add-Content { ${global:featuresEnvConfigContent}.Add($Value) } -ParameterFilter { $Path.EndsWith("features.env.config") } + } It 'Honors denylist' { $features = Get-EvaluatedFeatureFlags -predicate "important" -config $config @@ -627,5 +649,7 @@ Describe 'Out-EvaluatedFeaturesFiles' -Tag Features { } } -Remove-Module $ModuleName -Remove-Module test-functions \ No newline at end of file +AfterAll { + Remove-Module $ModuleName + Remove-Module test-functions +} \ No newline at end of file diff --git a/test/test-functions.psm1 b/test/test-functions.psm1 index d7ba745..d30554c 100644 --- a/test/test-functions.psm1 +++ b/test/test-functions.psm1 @@ -22,7 +22,7 @@ Function Test-StringArrays if ($null -eq $Actual) { - $Expected | Should Be $null + $Expected | Should -Be $null return } @@ -38,11 +38,11 @@ Function Test-StringArrays Write-Host "Expected: $Expected" -ForegroundColor Red } - $Actual.Count | Should Be $Expected.Count + $Actual.Count | Should -Be $Expected.Count for ($i = 0; $i -lt $Actual.Count; $i++) { - $Actual[$i] | Should Be $Expected[$i] + $Actual[$i] | Should -Be $Expected[$i] } } @@ -68,7 +68,7 @@ Function Test-ObjectArrays if ($Actual -eq $null) { - $Expected | Should Be $null + $Expected | Should -Be $null return } @@ -84,7 +84,7 @@ Function Test-ObjectArrays Write-Host "Expected: $Expected" -ForegroundColor Red } - $Actual.Count | Should Be $Expected.Count + $Actual.Count | Should -Be $Expected.Count for ($i = 0; $i -lt $Actual.Count; $i++) { @@ -114,13 +114,13 @@ Function Test-Hashtables if ($Expected -eq $null) { - $Actual | Should Be $null + $Actual | Should -Be $null return } if ($Actual -eq $null) { - $Expected | Should Be $null + $Expected | Should -Be $null return } @@ -129,7 +129,7 @@ Function Test-Hashtables # Redundant, but tells Pester we tested something # If the counts don't match, continue with the comparison so we contain # find out what's missing in the test error log - $Actual.Count | Should Be $Expected.Count + $Actual.Count | Should -Be $Expected.Count return } @@ -141,11 +141,11 @@ Function Test-Hashtables if ($null -eq $actualProperty.Value) { - $actualProperty.Value | Should Be $expectedValue + $actualProperty.Value | Should -Be $expectedValue } else { - $actualProperty.Value.GetType() | Should Be $expectedValue.GetType() + $actualProperty.Value.GetType() | Should -Be $expectedValue.GetType() if ($expectedValue.GetType().FullName -eq 'System.Collections.Hashtable') { @@ -164,12 +164,12 @@ Function Test-Hashtables else { # Just assert their lengths for now - $actualProperty.Value.Count | Should Be $expectedValue.Count + $actualProperty.Value.Count | Should -Be $expectedValue.Count } } else { - $actualProperty.Value | Should Be $expectedValue + $actualProperty.Value | Should -Be $expectedValue } } } @@ -187,11 +187,11 @@ Function Test-Hashtables if ($null -eq $expectedProperty.Value) { - $actualValue | Should Be $expectedProperty.Value + $actualValue | Should -Be $expectedProperty.Value } else { - $actualValue.GetType() | Should Be $expectedProperty.Value.GetType() + $actualValue.GetType() | Should -Be $expectedProperty.Value.GetType() if ($expectedProperty.Value.GetType().FullName -eq 'System.Collections.Hashtable') { @@ -210,12 +210,12 @@ Function Test-Hashtables else { # Just assert their lengths for now - $actualValue.Count | Should Be $expectedProperty.Value.Count + $actualValue.Count | Should -Be $expectedProperty.Value.Count } } else { - $actualValue | Should Be $expectedProperty.Value + $actualValue | Should -Be $expectedProperty.Value } } } diff --git a/tools/run-tests.ps1 b/tools/run-tests.ps1 index 4df9c83..7d063ba 100644 --- a/tools/run-tests.ps1 +++ b/tools/run-tests.ps1 @@ -1,17 +1,27 @@ # Mostly for use of CI/CD. Install Pester and run tests. +param ( + # Set to true to install Pester 5.1.0, regardless of whether a Pester version + # is present in the environment. + [switch] $InstallPester = $false +) + $parentDir = Split-Path -Parent (Split-Path -Parent $PSCommandPath) $testDir = Join-Path $parentDir -ChildPath "test" # Debug info. $PSVersionTable | Out-String -Write-Host "Checking for Pester > 4.0.0..." -$pester = Get-Module -ListAvailable | Where-Object {$_.Name -eq "Pester" -and $_.Version -gt '4.0.0'} -if ($pester.Count -eq 0) { - Write-Host "Cannot find the Pester module. Installing it." - Install-Module Pester -Force -Scope CurrentUser -RequiredVersion 4.10.1 -} else { - Write-Host "Found Pester version $($pester.Version)." +# List Pester versions. +$pesterVersions = Get-Module -ListAvailable | Where-Object {$_.Name -eq "Pester" } +$pesterVersions | % { Write-Host $_.Name $_.Version } + +if ($pesterVersions.Count -eq 0) { + Write-Warning "No Pester found, will install Pester 5.1.0" + $InstallPester = $true +} + +if ($InstallPester) { + Install-Module Pester -Force -Scope CurrentUser -RequiredVersion 5.1.0 } $FailedTests = Invoke-Pester $testDir -EnableExit -OutputFile "test/results.xml" -OutputFormat "NUnitXML" -CodeCoverage "$parentDir/FeatureFlags.psm1"