diff --git a/src/System.Management.Automation/engine/parser/Compiler.cs b/src/System.Management.Automation/engine/parser/Compiler.cs index d160ea6149..239525d2c3 100644 --- a/src/System.Management.Automation/engine/parser/Compiler.cs +++ b/src/System.Management.Automation/engine/parser/Compiler.cs @@ -1481,7 +1481,7 @@ namespace System.Management.Automation.Language private static RuntimeDefinedParameter GetRuntimeDefinedParameter(ParameterAst parameterAst, ref bool customParameterSet, ref bool usesCmdletBinding) { - List attributes = new List(parameterAst.Attributes.Count); + var attributes = new List(parameterAst.Attributes.Count); bool hasParameterAttribute = false; bool hasEnabledParamAttribute = false; bool hasSeenExpAttribute = false; @@ -1509,7 +1509,7 @@ namespace System.Management.Automation.Language usesCmdletBinding = true; if (paramAttribute.Position != int.MinValue || !paramAttribute.ParameterSetName.Equals(ParameterAttribute.AllParameterSets, - StringComparison.OrdinalIgnoreCase)) + StringComparison.OrdinalIgnoreCase)) { customParameterSet = true; } diff --git a/test/powershell/Language/Parser/TypeAccelerator.Tests.ps1 b/test/powershell/Language/Parser/TypeAccelerator.Tests.ps1 index c7b2cb0770..13133980d2 100644 --- a/test/powershell/Language/Parser/TypeAccelerator.Tests.ps1 +++ b/test/powershell/Language/Parser/TypeAccelerator.Tests.ps1 @@ -70,6 +70,18 @@ Describe "Type accelerators" -Tags "CI" { Accelerator = 'DscResource' Type = [System.Management.Automation.DscResourceAttribute] } + @{ + Accelerator = 'ExperimentAction' + Type = [System.Management.Automation.ExperimentAction] + } + @{ + Accelerator = 'Experimental' + Type = [System.Management.Automation.ExperimentalAttribute] + } + @{ + Accelerator = 'ExperimentalFeature' + Type = [System.Management.Automation.ExperimentalFeature] + } @{ Accelerator = 'float' Type = [System.Single] diff --git a/test/powershell/engine/Basic/DefaultCommands.Tests.ps1 b/test/powershell/engine/Basic/DefaultCommands.Tests.ps1 index 64b2b441c7..0fcdef96c7 100644 --- a/test/powershell/engine/Basic/DefaultCommands.Tests.ps1 +++ b/test/powershell/engine/Basic/DefaultCommands.Tests.ps1 @@ -258,6 +258,7 @@ Describe "Verify approved aliases list" -Tags "CI" { "Cmdlet", "Get-EventLog", , $($FullCLR ) "Cmdlet", "Get-EventSubscriber", , $($FullCLR -or $CoreWindows -or $CoreUnix) "Cmdlet", "Get-ExecutionPolicy", , $($FullCLR -or $CoreWindows -or $CoreUnix) +"Cmdlet", "Get-ExperimentalFeature", , $( $CoreWindows -or $CoreUnix) "Cmdlet", "Get-FileHash", , $( $CoreWindows -or $CoreUnix) "Cmdlet", "Get-FormatData", , $($FullCLR -or $CoreWindows -or $CoreUnix) "Cmdlet", "Get-Help", , $($FullCLR -or $CoreWindows -or $CoreUnix) diff --git a/test/powershell/engine/ExperimentalFeature/ExperimentalFeature.Basic.Tests.ps1 b/test/powershell/engine/ExperimentalFeature/ExperimentalFeature.Basic.Tests.ps1 new file mode 100644 index 0000000000..03688aa664 --- /dev/null +++ b/test/powershell/engine/ExperimentalFeature/ExperimentalFeature.Basic.Tests.ps1 @@ -0,0 +1,367 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +Describe "Experimental Feature Basic Tests - Feature-Disabled" -tags "CI" { + + BeforeAll { + $skipTest = $EnabledExperimentalFeatures.Contains('ExpTest.FeatureOne') + + if ($skipTest) { + Write-Verbose "Test Suite Skipped. The test suite requires the experimental feature 'ExpTest.FeatureOne' to be disabled." -Verbose + $originalDefaultParameterValues = $PSDefaultParameterValues.Clone() + $PSDefaultParameterValues["it:skip"] = $true + } else { + $TestModule = Join-Path $PSScriptRoot "assets" "ExpTest" + $AssemblyPath = Join-Path $TestModule "ExpTest.dll" + if (-not (Test-Path $AssemblyPath)) { + $SourcePath = Join-Path $TestModule "ExpTest.cs" + Add-Type -Path $SourcePath -OutputType Library -OutputAssembly $AssemblyPath + } + $moduleInfo = Import-Module $TestModule -PassThru + } + } + + AfterAll { + if ($skipTest) { + $global:PSDefaultParameterValues = $originalDefaultParameterValues + } else { + Remove-Module -ModuleInfo $moduleInfo -Force -ErrorAction SilentlyContinue + } + } + + It "No experimental features is enabled" { + $EnabledExperimentalFeatures.Count | Should -Be 0 + } + + It "Replace existing command - version one should be shown" -TestCases @( + @{ Name = "Invoke-AzureFunction"; CommandType = "Function" } + @{ Name = "Invoke-AzureFunctionCSharp"; CommandType = "Cmdlet" } + ) { + param($Name, $CommandType) + $command = Get-Command $Name + $command.CommandType | Should -Be $CommandType + $command.Source | Should -BeExactly $moduleInfo.Name + & $Name -Token "Token" -Command "Command" | Should -BeExactly "Invoke-AzureFunction Version ONE" + + if ($CommandType -eq "Function") { + $expectedErrorId = "CommandNotFoundException,Microsoft.PowerShell.Commands.GetCommandCommand" + { Get-Command "Invoke-AzureFunctionV2" -ErrorAction Stop } | Should -Throw -ErrorId $expectedErrorId + { & $moduleInfo { Get-Command "Invoke-AzureFunctionV2" -ErrorAction Stop } } | Should -Throw -ErrorId $expectedErrorId + } + } + + It "Experimental parameter set - '' should NOT have '-SwitchOne' and '-SwitchTwo'" -TestCases @( + @{ Name = "Get-GreetingMessage"; CommandType = "Function" } + @{ Name = "Get-GreetingMessageCSharp"; CommandType = "Cmdlet" } + ) { + param($Name, $CommandType) + $command = Get-Command $Name + $command.CommandType | Should -Be $CommandType + ## 11 common parameters + '-Name' + $command.Parameters.Count | Should -Be 12 + & $Name -Name Joe | Should -BeExactly "Hello World Joe." + } + + It "Experimental parameter set - '' should NOT have 'WebSocket' parameter set" -TestCases @( + @{ Name = "Invoke-MyCommand"; CommandType = "Function" } + @{ Name = "Invoke-MyCommandCSharp"; CommandType = "Cmdlet" } + ) { + param($Name, $CommandType) + $command = Get-Command $Name + $command.CommandType | Should -Be $CommandType + + ## 11 common parameters + '-UserName', '-ComputerName', '-ConfigurationName', '-VMName', '-Port', '-ThrottleLimit' and '-Command' + $command.Parameters.Count | Should -Be 18 + $command.ParameterSets.Count | Should -Be 2 + + $command.Parameters["UserName"].ParameterSets.Count | Should -Be 1 + $command.Parameters["UserName"].ParameterSets.ContainsKey("ComputerSet") | Should -Be $true + + $command.Parameters["ComputerName"].ParameterSets.Count | Should -Be 1 + $command.Parameters["ComputerName"].ParameterSets.ContainsKey("ComputerSet") | Should -Be $true + + $command.Parameters["ConfigurationName"].ParameterSets.Count | Should -Be 1 + $command.Parameters["ConfigurationName"].ParameterSets.ContainsKey("ComputerSet") | Should -Be $true + + $command.Parameters["VMName"].ParameterSets.Count | Should -Be 1 + $command.Parameters["VMName"].ParameterSets.ContainsKey("VMSet") | Should -Be $true + + $command.Parameters["Port"].ParameterSets.Count | Should -Be 1 + $command.Parameters["Port"].ParameterSets.ContainsKey("VMSet") | Should -Be $true + + $command.Parameters["ThrottleLimit"].ParameterSets.Count | Should -Be 1 + $command.Parameters["ThrottleLimit"].ParameterSets.ContainsKey("__AllParameterSets") | Should -Be $true + + $command.Parameters["Command"].ParameterSets.Count | Should -Be 1 + $command.Parameters["Command"].ParameterSets.ContainsKey("__AllParameterSets") | Should -Be $true + + ## 11 common parameters + '-UserName', '-ComputerName', '-ConfigurationName', '-ThrottleLimit' and '-Command' + $command.ParameterSets[0].Name | Should -BeExactly "ComputerSet" + $command.ParameterSets[0].Parameters.Count | Should -Be 16 + + ## 11 common parameters + '-VMName', '-Port', '-ThrottleLimit' and '-Command' + $command.ParameterSets[1].Name | Should -BeExactly "VMSet" + $command.ParameterSets[1].Parameters.Count | Should -Be 15 + + & $Name -UserName "user" -ComputerName "localhost" -ConfigurationName "config" | Should -BeExactly "Invoke-MyCommand with ComputerSet" + & $Name -VMName "VM" -Port "80" | Should -BeExactly "Invoke-MyCommand with VMSet" + } + + It "Experimental parameter set - '' should have '-SessionName' only" -TestCases @( + @{ Name = "Test-MyRemoting"; CommandType = "Function" } + @{ Name = "Test-MyRemotingCSharp"; CommandType = "Cmdlet" } + ) { + param($Name, $CommandType) + $command = Get-Command $Name + $command.CommandType | Should -Be $CommandType + ## 11 common parameters + '-SessionName' + $command.Parameters.Count | Should -Be 12 + $command.Parameters["SessionName"].ParameterType.FullName | Should -BeExactly "System.String" + $command.Parameters.ContainsKey("ComputerName") | Should -Be $false + } + + It "Use 'Experimental' attribute directly on parameters - ''" -TestCases @( + @{ Name = "Save-MyFile"; CommandType = "Function" } + @{ Name = "Save-MyFileCSharp"; CommandType = "Cmdlet" } + ) { + param($Name, $CommandType) + $command = Get-Command $Name + $command.CommandType | Should -Be $CommandType + ## 11 common parameters + '-ByUrl', '-ByRadio', '-FileName', '-Configuration' + $command.Parameters.Count | Should -Be 15 + $command.ParameterSets.Count | Should -Be 2 + + $command.Parameters["ByUrl"].ParameterSets.Count | Should -Be 1 + $command.Parameters["ByUrl"].ParameterSets.ContainsKey("UrlSet") | Should -Be $true + + $command.Parameters["ByRadio"].ParameterSets.Count | Should -Be 1 + $command.Parameters["ByRadio"].ParameterSets.ContainsKey("RadioSet") | Should -Be $true + + $command.Parameters["Configuration"].ParameterSets.Count | Should -Be 2 + $command.Parameters["Configuration"].ParameterSets.ContainsKey("UrlSet") | Should -Be $true + $command.Parameters["Configuration"].ParameterSets.ContainsKey("RadioSet") | Should -Be $true + + $command.Parameters["FileName"].ParameterSets.Count | Should -Be 1 + $command.Parameters["FileName"].ParameterSets.ContainsKey("__AllParameterSets") | Should -Be $true + + $command.Parameters.ContainsKey("Destination") | Should -Be $false + } + + It "Dynamic parameters - -" -TestCases @( + @{ Name = "Test-MyDynamicParamOne"; CommandType = "Function" } + @{ Name = "Test-MyDynamicParamOneCSharp"; CommandType = "Cmdlet" } + @{ Name = "Test-MyDynamicParamTwo"; CommandType = "Function" } + @{ Name = "Test-MyDynamicParamTwoCSharp"; CommandType = "Cmdlet" } + ) { + param($Name, $CommandType) + $command = Get-Command $Name + $command.CommandType | Should -Be $CommandType + ## 11 common parameters + '-Name' (dynamic parameters are not triggered) + $command.Parameters.Count | Should -Be 12 + $command.Parameters["Name"] | Should -Not -BeNullOrEmpty + + $command = Get-Command $Name -ArgumentList "Joe" + ## 11 common parameters + '-Name' and '-ConfigName' (dynamic parameters are triggered) + $command.Parameters.Count | Should -Be 13 + $command.Parameters["ConfigName"].Attributes.Count | Should -Be 2 + $command.Parameters["ConfigName"].Attributes[0] | Should -BeOfType [parameter] + $command.Parameters["ConfigName"].Attributes[1] | Should -BeOfType [ValidateNotNullOrEmpty] + + $command.Parameters.ContainsKey("ConfigFile") | Should -Be $false + } +} + +Describe "Experimental Feature Basic Tests - Feature-Enabled" -Tag "CI" { + + BeforeAll { + $skipTest = -not $EnabledExperimentalFeatures.Contains('ExpTest.FeatureOne') + + if ($skipTest) { + Write-Verbose "Test Suite Skipped. The test suite requires the experimental feature 'ExpTest.FeatureOne' to be enabled." -Verbose + $originalDefaultParameterValues = $PSDefaultParameterValues.Clone() + $PSDefaultParameterValues["it:skip"] = $true + } else { + $TestModule = Join-Path $PSScriptRoot "assets" "ExpTest" + $AssemblyPath = Join-Path $TestModule "ExpTest.dll" + if (-not (Test-Path $AssemblyPath)) { + $SourcePath = Join-Path $TestModule "ExpTest.cs" + Add-Type -Path $SourcePath -OutputType Library -OutputAssembly $AssemblyPath + } + $moduleInfo = Import-Module $TestModule -PassThru + } + } + + AfterAll { + if ($skipTest) { + $global:PSDefaultParameterValues = $originalDefaultParameterValues + } else { + Remove-Module -ModuleInfo $moduleInfo -Force -ErrorAction SilentlyContinue + } + } + + It "Experimental feature 'ExpTest.FeatureOne' should be enabled" { + $EnabledExperimentalFeatures.Count | Should -Be 1 + $EnabledExperimentalFeatures -contains "ExpTest.FeatureOne" | Should -Be $true + } + + It "Replace existing command - version two should be shown" -TestCases @( + @{ Name = "Invoke-AzureFunction"; CommandType = "Alias" } + @{ Name = "Invoke-AzureFunctionCSharp"; CommandType = "Cmdlet" } + ) { + param($Name, $CommandType) + $command = Get-Command $Name + $command.CommandType | Should -Be $CommandType + $command.Source | Should -BeExactly $moduleInfo.Name + & $Name -Token "Token" -Command "Command" | Should -BeExactly "Invoke-AzureFunction Version TWO" + + if ($CommandType -eq "Alias") { + $command.Definition | Should -Be "Invoke-AzureFunctionV2" + $expectedErrorId = "CommandNotFoundException,Microsoft.PowerShell.Commands.GetCommandCommand" + { Get-Command "Invoke-AzureFunction" -CommandType Function -ErrorAction Stop } | Should -Throw -ErrorId $expectedErrorId + { & $moduleInfo { Get-Command "Invoke-AzureFunction" -CommandType Function -ErrorAction Stop } } | Should -Throw -ErrorId $expectedErrorId + } + } + + It "Experimental parameter set - '' should have '-SwitchOne' and '-SwitchTwo'" -TestCases @( + @{ Name = "Get-GreetingMessage"; CommandType = "Function" } + @{ Name = "Get-GreetingMessageCSharp"; CommandType = "Cmdlet" } + ) { + param($Name, $CommandType) + $command = Get-Command $Name + $command.CommandType | Should -Be $CommandType + ## 11 common parameters + '-Name' + '-SwitchOne' + '-SwitchTwo' + $command.Parameters.Count | Should -Be 14 + $command.ParameterSets.Count | Should -Be 3 + + & $Name -Name Joe | Should -BeExactly "Hello World Joe." + & $Name -Name Joe -SwitchOne | Should -BeExactly "Hello World Joe.-SwitchOne is on." + & $Name -Name Joe -SwitchTwo | Should -BeExactly "Hello World Joe.-SwitchTwo is on." + } + + It "Experimental parameter set - '' should have 'WebSocket' parameter set" -TestCases @( + @{ Name = "Invoke-MyCommand"; CommandType = "Function" } + @{ Name = "Invoke-MyCommandCSharp"; CommandType = "Cmdlet" } + ) { + param($Name, $CommandType) + $command = Get-Command $Name + $command.CommandType | Should -Be $CommandType + + ## 11 common parameters + '-UserName', '-ComputerName', '-ConfigurationName', '-VMName', '-Port', + ## '-Token', '-WebSocketUrl', '-ThrottleLimit' and '-Command' + $command.Parameters.Count | Should -Be 20 + $command.ParameterSets.Count | Should -Be 3 + + $command.Parameters["UserName"].ParameterSets.Count | Should -Be 1 + $command.Parameters["UserName"].ParameterSets.ContainsKey("ComputerSet") | Should -Be $true + + $command.Parameters["ComputerName"].ParameterSets.Count | Should -Be 1 + $command.Parameters["ComputerName"].ParameterSets.ContainsKey("ComputerSet") | Should -Be $true + + $command.Parameters["VMName"].ParameterSets.Count | Should -Be 1 + $command.Parameters["VMName"].ParameterSets.ContainsKey("VMSet") | Should -Be $true + + $command.Parameters["Token"].ParameterSets.Count | Should -Be 1 + $command.Parameters["Token"].ParameterSets.ContainsKey("WebSocketSet") | Should -Be $true + + $command.Parameters["WebSocketUrl"].ParameterSets.Count | Should -Be 1 + $command.Parameters["WebSocketUrl"].ParameterSets.ContainsKey("WebSocketSet") | Should -Be $true + + $command.Parameters["ConfigurationName"].ParameterSets.Count | Should -Be 2 + $command.Parameters["ConfigurationName"].ParameterSets.ContainsKey("ComputerSet") | Should -Be $true + $command.Parameters["ConfigurationName"].ParameterSets.ContainsKey("WebSocketSet") | Should -Be $true + + $command.Parameters["Port"].ParameterSets.Count | Should -Be 2 + $command.Parameters["Port"].ParameterSets.ContainsKey("VMSet") | Should -Be $true + $command.Parameters["Port"].ParameterSets.ContainsKey("WebSocketSet") | Should -Be $true + + $command.Parameters["ThrottleLimit"].ParameterSets.Count | Should -Be 1 + $command.Parameters["ThrottleLimit"].ParameterSets.ContainsKey("__AllParameterSets") | Should -Be $true + + $command.Parameters["Command"].ParameterSets.Count | Should -Be 1 + $command.Parameters["Command"].ParameterSets.ContainsKey("__AllParameterSets") | Should -Be $true + + ## 11 common parameters + '-UserName', '-ComputerName', '-ConfigurationName', '-ThrottleLimit' and '-Command' + $command.ParameterSets[0].Name | Should -BeExactly "ComputerSet" + $command.ParameterSets[0].Parameters.Count | Should -Be 16 + + ## 11 common parameters + '-VMName', '-Port', '-ThrottleLimit' and '-Command' + $command.ParameterSets[1].Name | Should -BeExactly "VMSet" + $command.ParameterSets[1].Parameters.Count | Should -Be 15 + + ## 11 common parameters + '-Token', '-WebSocketUrl', '-ConfigurationName', '-Port', '-ThrottleLimit', '-Command' + $command.ParameterSets[2].Name | Should -BeExactly "WebSocketSet" + $command.ParameterSets[2].Parameters.Count | Should -Be 17 + + & $Name -UserName "user" -ComputerName "localhost" | Should -BeExactly "Invoke-MyCommand with ComputerSet" + & $Name -UserName "user" -ComputerName "localhost" -ConfigurationName "config" | Should -BeExactly "Invoke-MyCommand with ComputerSet" + + & $Name -VMName "VM" | Should -BeExactly "Invoke-MyCommand with VMSet" + & $Name -VMName "VM" -Port "80" | Should -BeExactly "Invoke-MyCommand with VMSet" + + & $Name -Token "token" -WebSocketUrl 'url' | Should -BeExactly "Invoke-MyCommand with WebSocketSet" + & $Name -Token "token" -WebSocketUrl 'url' -ConfigurationName 'config' -Port 80 | Should -BeExactly "Invoke-MyCommand with WebSocketSet" + } + + It "Experimental parameter set - '' should have '-ComputerName' only" -TestCases @( + @{ Name = "Test-MyRemoting"; CommandType = "Function" } + @{ Name = "Test-MyRemotingCSharp"; CommandType = "Cmdlet" } + ) { + param($Name, $CommandType) + $command = Get-Command $Name + $command.CommandType | Should -Be $CommandType + ## 11 common parameters + '-ComputerName' + $command.Parameters.Count | Should -Be 12 + $command.Parameters["ComputerName"].ParameterType.FullName | Should -BeExactly "System.String" + $command.Parameters.ContainsKey("SessionName") | Should -Be $false + } + + It "Use 'Experimental' attribute directly on parameters - ''" -TestCases @( + @{ Name = "Save-MyFile"; CommandType = "Function" } + @{ Name = "Save-MyFileCSharp"; CommandType = "Cmdlet" } + ) { + param($Name, $CommandType) + $command = Get-Command $Name + $command.CommandType | Should -Be $CommandType + ## 11 common parameters + '-ByUrl', '-ByRadio', '-FileName', '-Destination' + $command.Parameters.Count | Should -Be 15 + $command.ParameterSets.Count | Should -Be 2 + + $command.Parameters["ByUrl"].ParameterSets.Count | Should -Be 1 + $command.Parameters["ByUrl"].ParameterSets.ContainsKey("UrlSet") | Should -Be $true + + $command.Parameters["ByRadio"].ParameterSets.Count | Should -Be 1 + $command.Parameters["ByRadio"].ParameterSets.ContainsKey("RadioSet") | Should -Be $true + + $command.Parameters["Destination"].ParameterSets.Count | Should -Be 1 + $command.Parameters["Destination"].ParameterSets.ContainsKey("__AllParameterSets") | Should -Be $true + + $command.Parameters["FileName"].ParameterSets.Count | Should -Be 1 + $command.Parameters["FileName"].ParameterSets.ContainsKey("__AllParameterSets") | Should -Be $true + + $command.Parameters.ContainsKey("Configuration") | Should -Be $false + } + + It "Dynamic parameters - -" -TestCases @( + @{ Name = "Test-MyDynamicParamOne"; CommandType = "Function" } + @{ Name = "Test-MyDynamicParamOneCSharp"; CommandType = "Cmdlet" } + @{ Name = "Test-MyDynamicParamTwo"; CommandType = "Function" } + @{ Name = "Test-MyDynamicParamTwoCSharp"; CommandType = "Cmdlet" } + ) { + param($Name, $CommandType) + + $command = Get-Command $Name + $command.CommandType | Should -Be $CommandType + ## 11 common parameters + '-Name' (dynamic parameters are not triggered) + $command.Parameters.Count | Should -Be 12 + $command.Parameters["Name"] | Should -Not -BeNullOrEmpty + + $command = Get-Command $Name -ArgumentList "Joe" + ## 11 common parameters + '-Name' and '-ConfigFile' (dynamic parameters are triggered) + $command.Parameters.Count | Should -Be 13 + $command.Parameters["ConfigFile"].Attributes.Count | Should -Be 2 + $command.Parameters["ConfigFile"].Attributes[0] | Should -BeOfType [parameter] + $command.Parameters["ConfigFile"].Attributes[1] | Should -BeOfType [ValidateNotNullOrEmpty] + + $command.Parameters.ContainsKey("ConfigName") | Should -Be $false + } +} diff --git a/test/powershell/engine/ExperimentalFeature/Get-ExperimentalFeature.Tests.ps1 b/test/powershell/engine/ExperimentalFeature/Get-ExperimentalFeature.Tests.ps1 new file mode 100644 index 0000000000..4681b0660e --- /dev/null +++ b/test/powershell/engine/ExperimentalFeature/Get-ExperimentalFeature.Tests.ps1 @@ -0,0 +1,117 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +Describe "Get-ExperimentalFeature basic tests - Feature-Disabled" -tags "CI" { + + BeforeAll { + $skipTest = $EnabledExperimentalFeatures.Contains('ExpTest.FeatureOne') + + if ($skipTest) { + Write-Verbose "Test Suite Skipped. The test suite requires the experimental feature 'ExpTest.FeatureOne' to be disabled." -Verbose + $originalDefaultParameterValues = $PSDefaultParameterValues.Clone() + $PSDefaultParameterValues["it:skip"] = $true + } else { + Remove-Module -Name ExpTest -Force -ErrorAction SilentlyContinue + $testModulePath = Join-Path -Path $PSScriptRoot -ChildPath "assets" + $testModuleManifestPath = Join-Path -Path $testModulePath "ExpTest" "ExpTest.psd1" + $originalModulePath = $env:PSModulePath + $env:PSModulePath = $testModulePath + } + } + + AfterAll { + if ($skipTest) { + $global:PSDefaultParameterValues = $originalDefaultParameterValues + } else { + $env:PSModulePath = $originalModulePath + } + } + + It "'Get-ExperimentalFeature' should only return enabled features" { + $EnabledExperimentalFeatures.Count | Should -Be 0 + Get-ExperimentalFeature | Should -BeNullOrEmpty + } + + It "'Get-ExperimentalFeature -ListAvailable' should return all available features from module path" { + $features = Get-ExperimentalFeature "ExpTest*" -ListAvailable + $features | Should -Not -BeNullOrEmpty + $features[0].Name | Should -BeExactly "ExpTest.FeatureOne" + $features[0].Enabled | Should -Be $false + $features[0].Source | Should -BeExactly $testModuleManifestPath + + $features[1].Name | Should -BeExactly "ExpTest.FeatureTwo" + $features[1].Enabled | Should -Be $false + $features[1].Source | Should -BeExactly $testModuleManifestPath + } + + It "'Get-ExperimentalFeature -ListAvailable' pipeline input" { + $features = "ExpTest.FeatureOne", "ExpTest.FeatureTwo" | Get-ExperimentalFeature -ListAvailable + $features | Should -Not -BeNullOrEmpty + $features[0].Name | Should -BeExactly "ExpTest.FeatureOne" + $features[0].Enabled | Should -Be $false + $features[0].Source | Should -BeExactly $testModuleManifestPath + + $features[1].Name | Should -BeExactly "ExpTest.FeatureTwo" + $features[1].Enabled | Should -Be $false + $features[1].Source | Should -BeExactly $testModuleManifestPath + } +} + +Describe "Get-ExperimentalFeature basic tests - Feature-Enabled" -tags "CI" { + + BeforeAll { + $skipTest = -not $EnabledExperimentalFeatures.Contains('ExpTest.FeatureOne') + + if ($skipTest) { + Write-Verbose "Test Suite Skipped. The test suite requires the experimental feature 'ExpTest.FeatureOne' to be enabled." -Verbose + $originalDefaultParameterValues = $PSDefaultParameterValues.Clone() + $PSDefaultParameterValues["it:skip"] = $true + } else { + Remove-Module -Name ExpTest -Force -ErrorAction SilentlyContinue + $testModulePath = Join-Path -Path $PSScriptRoot -ChildPath "assets" + $testModuleManifestPath = Join-Path -Path $testModulePath "ExpTest" "ExpTest.psd1" + $originalModulePath = $env:PSModulePath + $env:PSModulePath = $testModulePath + } + } + + AfterAll { + if ($skipTest) { + $global:PSDefaultParameterValues = $originalDefaultParameterValues + } else { + $env:PSModulePath = $originalModulePath + } + } + + It "'Get-ExperimentalFeature' should return enabled features 'ExpTest.FeatureOne'" { + $EnabledExperimentalFeatures.Count | Should -Be 1 + $feature = Get-ExperimentalFeature "ExpTest.FeatureOne" + $feature | Should -Not -BeNullOrEmpty + $feature.Enabled | Should -Be $true + $feature.Source | Should -BeExactly $testModuleManifestPath + } + + It "'Get-ExperimentalFeature -ListAvailable' should return all available features from module path" { + $features = Get-ExperimentalFeature "ExpTest*" -ListAvailable + $features | Should -Not -BeNullOrEmpty + $features[0].Name | Should -BeExactly "ExpTest.FeatureOne" + $features[0].Enabled | Should -Be $true + $features[0].Source | Should -BeExactly $testModuleManifestPath + + $features[1].Name | Should -BeExactly "ExpTest.FeatureTwo" + $features[1].Enabled | Should -Be $false + $features[1].Source | Should -BeExactly $testModuleManifestPath + } + + It "'Get-ExperimentalFeature -ListAvailable' pipeline input" { + $features = "ExpTest.FeatureOne", "ExpTest.FeatureTwo" | Get-ExperimentalFeature -ListAvailable + $features | Should -Not -BeNullOrEmpty + $features[0].Name | Should -BeExactly "ExpTest.FeatureOne" + $features[0].Enabled | Should -Be $true + $features[0].Source | Should -BeExactly $testModuleManifestPath + + $features[1].Name | Should -BeExactly "ExpTest.FeatureTwo" + $features[1].Enabled | Should -Be $false + $features[1].Source | Should -BeExactly $testModuleManifestPath + } +} diff --git a/test/powershell/engine/ExperimentalFeature/assets/ExpTest/ExpTest.cs b/test/powershell/engine/ExperimentalFeature/assets/ExpTest/ExpTest.cs new file mode 100644 index 0000000000..af6fdb5c3f --- /dev/null +++ b/test/powershell/engine/ExperimentalFeature/assets/ExpTest/ExpTest.cs @@ -0,0 +1,216 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Management.Automation; + +namespace ExperimentalFeatureTest +{ + #region "Replace existing cmdlet" + + [Experimental("ExpTest.FeatureOne", ExperimentAction.Hide)] + [Cmdlet("Invoke", "AzureFunctionCSharp")] + public class InvokeAzureFunctionCommand : PSCmdlet + { + [Parameter] + public string Token { get; set; } + + [Parameter] + public string Command { get; set; } + + protected override void EndProcessing() + { + WriteObject("Invoke-AzureFunction Version ONE"); + } + } + + [Experimental("ExpTest.FeatureOne", ExperimentAction.Show)] + [Cmdlet("Invoke", "AzureFunctionCSharp")] + public class InvokeAzureFunctionCommandV2 : PSCmdlet + { + [Parameter(Mandatory = true)] + public string Token { get; set; } + + [Parameter(Mandatory = true)] + public string Command { get; set; } + + protected override void EndProcessing() + { + WriteObject("Invoke-AzureFunction Version TWO"); + } + } + + #endregion + + #region "Make parameter set experimental" + + [Cmdlet("Get", "GreetingMessageCSharp", DefaultParameterSetName = "Default")] + public class GetGreetingMessageCommand : PSCmdlet + { + [Parameter(Mandatory = true)] + public string Name { get; set; } + + [Parameter("ExpTest.FeatureOne", ExperimentAction.Show, ParameterSetName = "SwitchOneSet")] + public SwitchParameter SwitchOne { get; set; } + + [Parameter("ExpTest.FeatureOne", ExperimentAction.Show, ParameterSetName = "SwitchTwoSet")] + public SwitchParameter SwitchTwo { get; set; } + + protected override void EndProcessing() + { + string message = $"Hello World {Name}."; + if (ExperimentalFeature.HasEnabled("ExpTest.FeatureOne")) + { + if (SwitchOne.IsPresent) { message += "-SwitchOne is on."; } + if (SwitchTwo.IsPresent) { message += "-SwitchTwo is on."; } + } + WriteObject(message); + } + } + + [Cmdlet("Invoke", "MyCommandCSharp")] + public class InvokeMyCommandCommand : PSCmdlet + { + [Parameter(Mandatory = true, ParameterSetName = "ComputerSet")] + public string UserName { get; set; } + + [Parameter(Mandatory = true, ParameterSetName = "ComputerSet")] + public string ComputerName { get; set; } + + [Parameter(Mandatory = true, ParameterSetName = "VMSet")] + public string VMName { get; set; } + + // Enable web socket only if the feature is turned on. + [Parameter("ExpTest.FeatureOne", ExperimentAction.Show, Mandatory = true, ParameterSetName = "WebSocketSet")] + public string Token { get; set; } + + [Parameter("ExpTest.FeatureOne", ExperimentAction.Show, Mandatory = true, ParameterSetName = "WebSocketSet")] + public string WebSocketUrl { get; set; } + + // Add -ConfigurationName to parameter set "WebSocketSet" only if the feature is turned on. + [Parameter(ParameterSetName = "ComputerSet")] + [Parameter("ExpTest.FeatureOne", ExperimentAction.Show, ParameterSetName = "WebSocketSet")] + public string ConfigurationName { get; set; } + + // Add -Port to parameter set "WebSocketSet" only if the feature is turned on. + [Parameter(ParameterSetName = "VMSet")] + [Parameter("ExpTest.FeatureOne", ExperimentAction.Show, ParameterSetName = "WebSocketSet")] + public int Port { get; set; } + + [Parameter] + public int ThrottleLimit { get; set; } + + [Parameter] + public string Command { get; set; } + + protected override void EndProcessing() + { + switch (this.ParameterSetName) + { + case "ComputerSet": WriteObject("Invoke-MyCommand with ComputerSet"); break; + case "VMSet": WriteObject("Invoke-MyCommand with VMSet"); break; + case "WebSocketSet": WriteObject("Invoke-MyCommand with WebSocketSet"); break; + default: break; + } + } + } + + [Cmdlet("Test", "MyRemotingCSharp")] + public class TestMyRemotingCommand : PSCmdlet + { + // Replace one parameter with another one when the feature is turned on. + [Parameter("ExpTest.FeatureOne", ExperimentAction.Hide)] + public string SessionName { get; set; } + + [Parameter("ExpTest.FeatureOne", ExperimentAction.Show)] + public string ComputerName { get; set; } + + protected override void EndProcessing() { } + } + + #endregion + + #region "Use 'Experimental' attribute on parameters" + + [Cmdlet("Save", "MyFileCSharp")] + public class SaveMyFileCommand : PSCmdlet + { + [Parameter(ParameterSetName = "UrlSet")] + public SwitchParameter ByUrl { get; set; } + + [Parameter(ParameterSetName = "RadioSet")] + public SwitchParameter ByRadio { get; set; } + + [Parameter] + public string FileName { get; set; } + + [Experimental("ExpTest.FeatureOne", ExperimentAction.Show)] + [Parameter] + public string Destination { get; set; } + + [Experimental("ExpTest.FeatureOne", ExperimentAction.Hide)] + [Parameter(ParameterSetName = "UrlSet")] + [Parameter(ParameterSetName = "RadioSet")] + public string Configuration { get; set; } + + protected override void EndProcessing() { } + } + + #endregion + + #region "Dynamic parameters" + + public class DynamicParamOne + { + [Parameter("ExpTest.FeatureOne", ExperimentAction.Show)] + [ValidateNotNullOrEmpty] + public string ConfigFile { get; set; } + + [Parameter("ExpTest.FeatureOne", ExperimentAction.Hide)] + [ValidateNotNullOrEmpty] + public string ConfigName { get; set; } + } + + [Cmdlet("Test", "MyDynamicParamOneCSharp")] + public class TestMyDynamicParamOneCommand : PSCmdlet, IDynamicParameters + { + [Parameter(Position = 0)] + public string Name { get; set; } + + public object GetDynamicParameters() + { + return Name == "Joe" ? new DynamicParamOne() : null; + } + + protected override void EndProcessing() { } + } + + public class DynamicParamTwo + { + [Experimental("ExpTest.FeatureOne", ExperimentAction.Show)] + [Parameter()] + [ValidateNotNullOrEmpty] + public string ConfigFile { get; set; } + + [Experimental("ExpTest.FeatureOne", ExperimentAction.Hide)] + [Parameter()] + [ValidateNotNullOrEmpty] + public string ConfigName { get; set; } + } + + [Cmdlet("Test", "MyDynamicParamTwoCSharp")] + public class TestMyDynamicParamTwoCommand : PSCmdlet, IDynamicParameters + { + [Parameter(Position = 0)] + public string Name { get; set; } + + public object GetDynamicParameters() + { + return Name == "Joe" ? new DynamicParamTwo() : null; + } + + protected override void EndProcessing() { } + } + + #endregion +} \ No newline at end of file diff --git a/test/powershell/engine/ExperimentalFeature/assets/ExpTest/ExpTest.psd1 b/test/powershell/engine/ExperimentalFeature/assets/ExpTest/ExpTest.psd1 new file mode 100644 index 0000000000..ad613c7126 --- /dev/null +++ b/test/powershell/engine/ExperimentalFeature/assets/ExpTest/ExpTest.psd1 @@ -0,0 +1,43 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +# Module manifest for module 'ExpTest' + +@{ + +# Version number of this module. +ModuleVersion = '0.0.1' + +# Supported PSEditions +CompatiblePSEditions = @('Core') + +# ID used to uniquely identify this module +GUID = '109f75d1-38c1-46b3-8995-e80661ce822d' + +# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. +FunctionsToExport = if ($EnabledExperimentalFeatures -contains "ExpTest.FeatureOne") { + 'Invoke-AzureFunctionV2', 'Get-GreetingMessage', 'Invoke-MyCommand', 'Test-MyRemoting', 'Save-MyFile', 'Test-MyDynamicParamOne', 'Test-MyDynamicParamTwo' +} else { + 'Invoke-AzureFunction', 'Get-GreetingMessage', 'Invoke-MyCommand', 'Test-MyRemoting', 'Save-MyFile', 'Test-MyDynamicParamOne', 'Test-MyDynamicParamTwo' +} + +# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. +CmdletsToExport = 'Invoke-AzureFunctionCSharp', 'Get-GreetingMessageCSharp', 'Invoke-MyCommandCSharp', 'Test-MyRemotingCSharp', 'Save-MyFileCSharp', 'Test-MyDynamicParamOneCSharp', 'Test-MyDynamicParamTwoCSharp' + +# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. +AliasesToExport = @(if ($EnabledExperimentalFeatures -contains "ExpTest.FeatureOne") { 'Invoke-AzureFunction' }) + +# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess +NestedModules = @('ExpTest.psm1', 'ExpTest.dll') + +# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. +PrivateData = @{ + PSData = @{ + ExperimentalFeatures = @( + @{ Name = 'ExpTest.FeatureOne'; Description = "Test feature number one." } + @{ Name = 'ExpTest.FeatureTwo'; Description = "Test feature number two." } + ) + } +} + +} diff --git a/test/powershell/engine/ExperimentalFeature/assets/ExpTest/ExpTest.psm1 b/test/powershell/engine/ExperimentalFeature/assets/ExpTest/ExpTest.psm1 new file mode 100644 index 0000000000..3f2c1a08f2 --- /dev/null +++ b/test/powershell/engine/ExperimentalFeature/assets/ExpTest/ExpTest.psm1 @@ -0,0 +1,206 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +using namespace System.Management.Automation + +#region "Replace existing function" + +function Invoke-AzureFunction +{ + [Experimental("ExpTest.FeatureOne", [ExperimentAction]::Hide)] + param( + [string] $Token, + [string] $Command + ) + + "Invoke-AzureFunction Version ONE" +} + +function Invoke-AzureFunctionV2 +{ + [Experimental("ExpTest.FeatureOne", [ExperimentAction]::Show)] + [Alias("Invoke-AzureFunction")] + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string] $Token, + + [Parameter(Mandatory)] + [string] $Command + ) + + "Invoke-AzureFunction Version TWO" +} + +#endregion + +#region "Make parameter set experimental" + +function Get-GreetingMessage +{ + [CmdletBinding(DefaultParameterSetName = "Default")] + param( + [Parameter(Mandatory)] + [string] $Name, + + ## If only one parameter attribute is declared for a parameter, then the parameter is + ## hidden when the parameter attribute needs to be hide. + [Parameter("ExpTest.FeatureOne", [ExperimentAction]::Show, ParameterSetName = "SwitchOneSet")] + [switch] $SwitchOne, + + [Parameter("ExpTest.FeatureOne", [ExperimentAction]::Show, ParameterSetName = "SwitchTwoSet")] + [switch] $SwitchTwo + ) + + $message = "Hello World $Name." + + if ([ExperimentalFeature]::HasEnabled("ExpTest.FeatureOne")) + { + if ($SwitchOne) { $message += "-SwitchOne is on." } + if ($SwitchTwo) { $message += "-SwitchTwo is on." } + } + + Write-Output $message +} + +function Invoke-MyCommand +{ + param( + [Parameter(Mandatory, ParameterSetName = "ComputerSet")] + [string] $UserName, + [Parameter(Mandatory, ParameterSetName = "ComputerSet")] + [string] $ComputerName, + + [Parameter(Mandatory, ParameterSetName = "VMSet")] + [string] $VMName, + + ## Enable web socket only if the feature is turned on. + [Parameter("ExpTest.FeatureOne", [ExperimentAction]::Show, Mandatory, ParameterSetName = "WebSocketSet")] + [string] $Token, + [Parameter("ExpTest.FeatureOne", [ExperimentAction]::Show, Mandatory, ParameterSetName = "WebSocketSet")] + [string] $WebSocketUrl, + + ## Add -ConfigurationName to parameter set "WebSocketSet" only if the feature is turned on. + [Parameter(ParameterSetName = "ComputerSet")] + [Parameter("ExpTest.FeatureOne", [ExperimentAction]::Show, ParameterSetName = "WebSocketSet")] + [string] $ConfigurationName, + + ## Add -Port to parameter set "WebSocketSet" only if the feature is turned on. + [Parameter(ParameterSetName = "VMSet")] + [Parameter("ExpTest.FeatureOne", [ExperimentAction]::Show, ParameterSetName = "WebSocketSet")] + [int] $Port, + + [int] $ThrottleLimit, + [string] $Command + ) + + switch ($PSCmdlet.ParameterSetName) + { + "ComputerSet" { "Invoke-MyCommand with ComputerSet" } + "VMSet" { "Invoke-MyCommand with VMSet" } + "WebSocketSet" { "Invoke-MyCommand with WebSocketSet" } + } +} + +function Test-MyRemoting +{ + param( + ## Replace one parameter with another one when the feature is turned on. + [Parameter("ExpTest.FeatureOne", [ExperimentAction]::Hide)] + [string] $SessionName, + + [Parameter("ExpTest.FeatureOne", [ExperimentAction]::Show)] + [string] $ComputerName + ) +} + +#endregion + +#region "Use 'Experimental' attribute on parameters" + +function Save-MyFile +{ + param( + [Parameter(ParameterSetName = "UrlSet")] + [switch] $ByUrl, + + [Parameter(ParameterSetName = "RadioSet")] + [switch] $ByRadio, + + [string] $FileName, + + [Experimental("ExpTest.FeatureOne", [ExperimentAction]::Show)] + [string] $Destination, + + [Experimental("ExpTest.FeatureOne", [ExperimentAction]::Hide)] + [Parameter(ParameterSetName = "UrlSet")] + [Parameter(ParameterSetName = "RadioSet")] + [string] $Configuration + ) +} + +#endregion + +#region "Dynamic parameters" + +function Test-MyDynamicParamOne +{ + [CmdletBinding()] + param( + [string] $Name + ) + + ## Use the parameter attribute to hide or show a dynamic parameter. + DynamicParam { + if ($Name -eq "Joe") { + $runtimeParams = [RuntimeDefinedParameterDictionary]::new() + + $configFileAttributes = [System.Collections.ObjectModel.Collection[Attribute]]::new() + $configFileAttributes.Add([Parameter]::new("ExpTest.FeatureOne", [ExperimentAction]::Show)) + $configFileAttributes.Add([ValidateNotNullOrEmpty]::new()) + $configFileParam = [RuntimeDefinedParameter]::new("ConfigFile", [string], $configFileAttributes) + + $configNameAttributes = [System.Collections.ObjectModel.Collection[Attribute]]::new() + $configNameAttributes.Add([Parameter]::new("ExpTest.FeatureOne", [ExperimentAction]::Hide)) + $configNameAttributes.Add([ValidateNotNullOrEmpty]::new()) + $ConfigNameParam = [RuntimeDefinedParameter]::new("ConfigName", [string], $configNameAttributes) + + $runtimeParams.Add("ConfigFile", $configFileParam) + $runtimeParams.Add("ConfigName", $ConfigNameParam) + return $runtimeParams + } + } +} + +function Test-MyDynamicParamTwo +{ + [CmdletBinding()] + param( + [string] $Name + ) + + ## Use the experimental attribute to hide or show a dynamic parameter. + DynamicParam { + if ($Name -eq "Joe") { + $runtimeParams = [RuntimeDefinedParameterDictionary]::new() + + $configFileAttributes = [System.Collections.ObjectModel.Collection[Attribute]]::new() + $configFileAttributes.Add([Experimental]::new("ExpTest.FeatureOne", [ExperimentAction]::Show)) + $configFileAttributes.Add([Parameter]::new()) + $configFileAttributes.Add([ValidateNotNullOrEmpty]::new()) + $configFileParam = [RuntimeDefinedParameter]::new("ConfigFile", [string], $configFileAttributes) + + $configNameAttributes = [System.Collections.ObjectModel.Collection[Attribute]]::new() + $configNameAttributes.Add([Experimental]::new("ExpTest.FeatureOne", [ExperimentAction]::Hide)) + $configNameAttributes.Add([Parameter]::new()) + $configNameAttributes.Add([ValidateNotNullOrEmpty]::new()) + $ConfigNameParam = [RuntimeDefinedParameter]::new("ConfigName", [string], $configNameAttributes) + + $runtimeParams.Add("ConfigFile", $configFileParam) + $runtimeParams.Add("ConfigName", $ConfigNameParam) + return $runtimeParams + } + } +} + +#endregion