PowerShell/test/powershell/Language/Classes/Scripting.Classes.Attributes.Tests.ps1
Ilya dc76c86f7a ValidateSetAttribute enhancement: support set values to be dynamically generated from a custom ValidateSetValueGenerator (#3784)
Currently `ValidateSetAttribute` accepts only explicit constants as valid values. This is a significant limitation. Sometimes we need to get valid values dynamically, ex., Azure VMs, logged-on users and so on. The PR add follow possibilities:
- pass a _custom type_ (a valid values generator) implementing `IValidateSetValuesGenerator` interface to get valid values dynamically.
- pass a _custom type_ (a valid values generator) derived from `CachedValidValuesGeneratorBase` abstract class to get valid values dynamically and _cache_ the list to share with other ValidateSetAttribute attributes.
2017-07-13 21:28:32 -07:00

479 lines
16 KiB
PowerShell

Describe 'Attributes Test' -Tags "CI" {
BeforeAll {
$dummyAttributesSource = @'
using System.Management.Automation;
namespace Dummy
{
public class DoubleStringTransformationAttribute : ArgumentTransformationAttribute
{
public override object Transform(EngineIntrinsics engineIntrinsics, object inputData)
{
string arg = inputData as string;
if (arg != null)
{
return arg + arg;
}
return inputData;
}
}
public class AppendStringTransformationAttribute : ArgumentTransformationAttribute
{
public override object Transform(EngineIntrinsics engineIntrinsics, object inputData)
{
string arg = inputData as string;
if (arg != null)
{
return arg + "___";
}
return inputData;
}
}
public class DoubleInt : ArgumentTransformationAttribute
{
public override object Transform(EngineIntrinsics engineIntrinsics, object inputData)
{
int? arg = inputData as int?;
if (arg != null)
{
return arg + arg;
}
return inputData;
}
}
}
'@
Add-Type -TypeDefinition $dummyAttributesSource
}
Context 'Property.Instance.ValidateSet.String' {
class C1 { [ValidateSet("Present", "Absent")][string]$Ensure }
# This call should not throw exception
[C1]::new().Ensure = "Present"
It 'Error when ValidateSet should be ExceptionWhenSetting' {
try
{
[C1]::new().Ensure = "foo"
throw "Exception expected"
}
catch
{
$_.FullyQualifiedErrorId | Should be 'ExceptionWhenSetting'
}
}
}
Context 'Property.Static.ValidateSet.String' {
class C1 { static [ValidateSet("Present", "Absent")][string]$Ensure }
# This call should not throw exception
[C1]::Ensure = "Present"
It 'Error when ValidateSet should be ExceptionWhenSetting'{
try {
[C1]::Ensure = "foo"
throw "Exception expected"
}
catch {
$_.FullyQualifiedErrorId | Should be 'ExceptionWhenSetting'
}
}
}
Context 'Property.Instance.ValidateRange.Int' {
class C1 { [ValidateRange(1, 10)][int]$f }
# This call should not throw exception
[C1]::new().f = 10
[C1]::new().f = 1
It 'Error when ValidateSet should be ExceptionWhenSetting'{
try {
[C1]::new().f = 20
throw "Exception expected"
}
catch {
$_.FullyQualifiedErrorId | Should be 'ExceptionWhenSetting'
}
}
}
Context 'Property.Static.ValidateRange.Int' {
class C1 { static [ValidateRange(1, 10)][int]$f }
# This call should not throw exception
[C1]::f = 5
It 'Error when ValidateSet should be ExceptionWhenSetting'{
try {
[C1]::f = 20
throw "Exception expected"
}
catch {
$_.FullyQualifiedErrorId | Should be 'ExceptionWhenSetting'
}
}
}
Context 'Property.Static.ValidateSet.ImplicitObject' {
class C1 { static [ValidateSet("abc", 5)]$o }
# This call should not throw exception
[C1]::o = "abc"
[C1]::o = 5
It 'Error when ValidateSet should be ExceptionWhenSetting'{
try {
[C1]::o = 1
throw "Exception expected"
}
catch {
$_.FullyQualifiedErrorId | Should be 'ExceptionWhenSetting'
}
}
}
#
# We use [scriptblock]::Create() here to allow SuiteSetup add Dummy.Transformation type to
# the scope. Otherwise, we will need to have all classes for attributes in parse time.
#
# Invoke() returns an array, we need first element of it.
#
Context 'Property.Instance.Transformation.ImplicitObject' {
$c = [scriptblock]::Create('class C1 { [Dummy.DoubleStringTransformation()]$arg }; [C1]::new()').Invoke()[0]
It 'Implicitly Transform to 100' {
$c.arg = 100
$c.arg | should be 100
}
It 'Implicitly Transform to foo' {
$c.arg = "foo"
$c.arg | should be "foofoo"
}
}
Context 'Property.Instance.Transformation.String' {
$c = [scriptblock]::Create('class C1 { [Dummy.DoubleStringTransformation()][string]$arg }; [C1]::new()').Invoke()[0]
It 'set to foo' {
$c.arg = "foo"
$c.arg | should be "foofoo"
}
}
Context Property.Instance.Transformation.Int {
$c = [scriptblock]::Create('class C1 { [Dummy.DoubleInt()][int]$arg }; [C1]::new()').Invoke()[0]
It 'arg should be 200' {
$c.arg = 100
$c.arg | should be 200
}
It 'Set to string should fail with ExceptionWhenSetting' {
try {
$c.arg = "abc"
throw "Exception expected"
}
catch {
$_.FullyQualifiedErrorId | Should be 'ExceptionWhenSetting'
}
}
}
Context Property.Instance.Transformation.Nullable {
$c = [scriptblock]::Create('class C1 { [Nullable[int]][Dummy.DoubleStringTransformation()]$arg }; [C1]::new()').Invoke()[0]
It 'arg should be 100' {
$c.arg = 100
$c.arg | should be 100
}
}
Context Property.Instance.Transformation.Order {
$c = [scriptblock]::Create('class C1 { [Dummy.DoubleStringTransformation()][Dummy.AppendStringTransformation()]$arg }; [C1]::new()').Invoke()[0]
It 'arg should be 100' {
$c.arg = 100
$c.arg | should be 100
}
It 'arg should be foo___foo___g' {
$c.arg = "foo"
$c.arg | should be "foo___foo___"
}
}
}
Describe 'Type resolution with attributes' -Tag "CI" {
# There is kind of a collision between names
# System.Diagnostics.Tracing.EventSource
# System.Diagnostics.Tracing.EventSourceAttribute
# We need to make sure that we resolve type name to the right class at each usage
Context 'Name collision' {
It 'Resolve System.Diagnostics.Tracing.EventSource to Attribute and to Type in the different contexts' {
[System.Diagnostics.Tracing.EventSource(Name = "MyPSEventSource")]
class MyEventSource : System.Diagnostics.Tracing.EventSource
{
[void] OnEvent([string]$Message) {}
}
[MyEventSource]::new() | Should Not Be $null
}
}
}
Describe 'ValidateSet support a dynamically generated set' -Tag "CI" {
Context 'C# tests' {
BeforeAll {
$a=@'
using System;
using System.Management.Automation;
using System.Collections.Generic;
namespace Test.Language {
[Cmdlet(VerbsCommon.Get, "TestValidateSet0")]
public class TestValidateSetCommand0 : PSCmdlet
{
[Parameter]
[ValidateSet(typeof(PSCmdlet))]
public string Param1;
protected override void EndProcessing()
{
WriteObject(Param1);
}
}
[Cmdlet(VerbsCommon.Get, "TestValidateSet4")]
public class TestValidateSetCommand4 : PSCmdlet
{
[Parameter]
[ValidateSet(typeof(GenValuesForParam))]
public string Param1;
protected override void EndProcessing()
{
WriteObject(Param1);
}
}
[Cmdlet(VerbsCommon.Get, "TestValidateSet5")]
public class TestValidateSetCommand5 : PSCmdlet
{
[Parameter]
[ValidateSet(typeof(GenValuesForParamNull))]
public string Param1;
protected override void EndProcessing()
{
WriteObject(Param1);
}
}
/// Implement of test IValidateSetValuesGenerator
public class GenValuesForParamNull : IValidateSetValuesGenerator
{
public string[] GetValidValues()
{
var testValues = new string[] {"Test1","TestString1","Test2"};
return null;
}
}
public class GenValuesForParam : IValidateSetValuesGenerator
{
public string[] GetValidValues()
{
var testValues = new string[] {"Test1","TestString1","Test2"};
return testValues;
}
}
}
'@
$testAssemply = "$TestDrive\tst-$(New-Guid).dll"
Add-Type -TypeDefinition $a -OutputAssembly $testAssemply
Import-Module $testAssemply
}
It 'Throw if IValidateSetValuesGenerator is not implemented' {
{ Get-TestValidateSet0 -Param1 "TestString" -ErrorAction Stop } | ShouldBeErrorId "Argument"
}
It 'Dynamically generated set works in C# with default (immediate) cache expire' {
Get-TestValidateSet4 -Param1 "TestString1" -ErrorAction SilentlyContinue | Should BeExactly "TestString1"
}
It 'Empty dynamically generated set throws in C#' {
$exc = {
Get-TestValidateSet5 -Param1 "TestString1" -ErrorAction Stop
} | ShouldBeErrorId "ParameterArgumentValidationError,Test.Language.TestValidateSetCommand5"
$exc.Exception.InnerException.ErrorRecord.FullyQualifiedErrorId | Should BeExactly "ValidateSetGeneratedValidValuesListIsNull"
}
}
Context 'Powershell tests' {
BeforeAll {
class GenValuesForParam : System.Management.Automation.IValidateSetValuesGenerator {
[String[]] GetValidValues() {
return [string[]]("Test1","TestString1","Test2")
}
}
class GenValuesForParamNull : System.Management.Automation.IValidateSetValuesGenerator {
[String[]] GetValidValues() {
return [string[]]$null
}
}
# Return '$testValues2' and after 2 seconds after first use return another array '$testValues1'.
class GenValuesForParamCache1 : System.Management.Automation.IValidateSetValuesGenerator {
[String[]] GetValidValues() {
$testValues1 = "Test11","TestString11","Test22"
$testValues2 = "Test11","TestString22","Test22"
$currentTime = [DateTime]::Now
if ([DateTime]::Compare([GenValuesForParamCache1]::cacheTime, $currentTime) -le 0)
{
$testValues = $testValues1;
}
else
{
$testValues = $testValues2;
}
return [string[]]$testValues
}
static [DateTime] $cacheTime = [DateTime]::Now.AddSeconds(2);
}
function Get-TestValidateSetPS4
{
[CmdletBinding()]
Param
(
[ValidateSet([GenValuesForParam])]
$Param1
)
$Param1
}
function Get-TestValidateSetPS5
{
[CmdletBinding()]
Param
(
[ValidateSet([GenValuesForParamNull])]
$Param1
)
$Param1
}
function Get-TestValidateSetPS6
{
[CmdletBinding()]
Param
(
[ValidateSet([UnImplementedGeneratorOfValues])]
$Param1
)
$Param1
}
}
It 'Dynamically generated set works in PowerShell script with default (immediate) cache expire' {
Get-TestValidateSetPS4 -Param1 "TestString1" -ErrorAction SilentlyContinue | Should BeExactly "TestString1"
}
It 'Empty dynamically generated set throws in PowerShell script' {
$exc = {
Get-TestValidateSetPS5 -Param1 "TestString1" -ErrorAction Stop
} | ShouldBeErrorId "ParameterArgumentValidationError,Get-TestValidateSetPS5"
$exc.Exception.InnerException.ErrorRecord.FullyQualifiedErrorId | Should BeExactly "ValidateSetGeneratedValidValuesListIsNull"
}
It 'Unimplemented valid values generator type throws in PowerShell script' {
{
Get-TestValidateSetPS6 -Param1 "AnyTestString" -ErrorAction Stop
} | ShouldBeErrorId "TypeNotFound"
}
}
Context 'CachedValidValuesGeneratorBase class tests' {
BeforeAll {
class GenValuesForParam : System.Management.Automation.CachedValidValuesGeneratorBase {
GenValuesForParam() : base(300) {
}
[String[]] GenerateValidValues() {
return [string[]]("Test1","TestString1","Test2")
}
}
class GenValuesWithExpiration : System.Management.Automation.CachedValidValuesGeneratorBase {
GenValuesWithExpiration() : base(2) {
}
Static [bool] $temp = $true;
[String[]] GenerateValidValues() {
if ([GenValuesWithExpiration]::temp) {
[GenValuesWithExpiration]::temp = $false
return [string[]]("Test1","TestString1","Test2")
} else {
[GenValuesWithExpiration]::temp = $true
return [string[]]("Test1","TestString2","Test2")
}
}
}
function Get-TestValidateSetPS4
{
[CmdletBinding()]
Param
(
[ValidateSet([GenValuesForParam])]
$Param1
)
$Param1
}
function Get-TestValidateSetPS5
{
[CmdletBinding()]
Param
(
[ValidateSet([GenValuesWithExpiration])]
$Param1
)
$Param1
}
}
It 'Can implement CachedValidValuesGeneratorBase in PowerShell' {
Get-TestValidateSetPS4 -Param1 "TestString1" -ErrorAction SilentlyContinue | Should BeExactly "TestString1"
}
It 'Can implement CachedValidValuesGeneratorBase with cache expiration in PowerShell' {
Get-TestValidateSetPS5 -Param1 "TestString1" -ErrorAction SilentlyContinue | Should BeExactly "TestString1"
Get-TestValidateSetPS5 -Param1 "TestString1" -ErrorAction SilentlyContinue | Should BeExactly "TestString1"
Start-Sleep 3
Get-TestValidateSetPS5 -Param1 "TestString2" -ErrorAction SilentlyContinue | Should BeExactly "TestString2"
}
}
}