From 025c095debe67bbcca03c69264c03987a92f67e8 Mon Sep 17 00:00:00 2001 From: "Joel Sallow (/u/ta11ow)" <32407840+vexx32@users.noreply.github.com> Date: Fri, 6 Mar 2020 07:53:42 -0600 Subject: [PATCH] (#372) Fix issues in AboutEnumeration (#373) * :recycle: Cleanup old test file Refactor slightly, match file more to current style. Rename a few tests to make more sense. * :white_check_mark: Add regression test We can test file ASTs during our static analysis checks. Added a test to check we don't have any nested It blocks in the file. * :bug: (#372) Fix missing / extra brace * :memo: :art: Adjust AboutEnumeration formatting * :bug: (#372) Delay parsing of enum Using a raw scriptblock will break the parser in earlier versions. Fix is to define it as a string and parse that just before execution. --- .../AboutEnumerations.Koans.ps1 | 924 +++++++++--------- Tests/KoanValidation.Tests.ps1 | 150 ++- 2 files changed, 592 insertions(+), 482 deletions(-) diff --git a/PSKoans/Koans/Constructs and Patterns/AboutEnumerations.Koans.ps1 b/PSKoans/Koans/Constructs and Patterns/AboutEnumerations.Koans.ps1 index 9f7ce62cf..5785373c0 100644 --- a/PSKoans/Koans/Constructs and Patterns/AboutEnumerations.Koans.ps1 +++ b/PSKoans/Koans/Constructs and Patterns/AboutEnumerations.Koans.ps1 @@ -4,625 +4,675 @@ param() <# About Enumerations - Enumerations, or Enums, are used to give names to lists of constant values. Enumerations are very widely - used in the .NET framework and frequently encountered in PowerShell. - - Examples in this topic use Start-Job to avoid problems when continually loading a type in the current - PowerShell session. Enumeration values demonstrated in this section are cast to String within the jobs, - otherwise PowerShell will return only the numeric value from the job. + Enumerations, or Enums, are used to give names to lists of constant values. + Enumerations are very widely used in the .NET framework and frequently + encountered in PowerShell. + + Examples in this topic use Start-Job to avoid problems when continually + loading a type in the current PowerShell session. Enumeration values + demonstrated in this section are cast to String within the jobs, otherwise + PowerShell will return only the numeric value from the job. #> Describe 'About Enumerations' { - <# - The DayOfWeek enumeration (enum) which holds values describing the days of the week is a simple - example of an existing .NET enumeration. - #> - - Context 'Using Enumerations' { - It 'exposes the values of an enumeration in a number of different ways' { - <# - The most direct way to refer to enumeration values is with the static member access operator. - - If Strict Mode (Set-StrictMode) has been enabled, accessing a non-existent value will raise - an error. - #> + <# + The DayOfWeek enumeration (enum) which holds values describing the days + of the week is a simple example of an existing .NET enumeration. + #> - [DayOfWeek]::____ | Should -BeOfType [DayOfWeek] + Context 'Using Enumerations' { + It 'exposes the values of an enumeration in a number of different ways' { + <# + The most direct way to refer to enumeration values is with the + static member access operator. - <# - We can also cast a string to the enumeration type. + If Strict Mode (Set-StrictMode) has been enabled, accessing a + non-existent value will raise an error. + #> - Casting to a non-existent value always raises an error. - #> + [DayOfWeek]::____ | Should -BeOfType [DayOfWeek] - { [DayOfWeek]'____' } | Should -Not -Throw + <# + We can also cast a string to the enumeration type. - <# - We can also use the -as operator to do a "safe cast" to the enumeration type. + Casting to a non-existent value always raises an error. + #> - Using -as with a non-existent value will simply return $null. - #> + { [DayOfWeek]'____' } | Should -Not -Throw - '____' -as [DayOfWeek] | Should -BeOfType [DayOfWeek] - } + <# + We can also use the -as operator to do a "safe cast" to the + enumeration type. - It 'will find a match from a partial name if it can' { - <# - PowerShell will match an enum value based on a partial name. The named used - must match a unique value in the enum. + Using -as with a non-existent value will simply return $null. + #> - Try making Name less than the whole word. - #> + '____' -as [DayOfWeek] | Should -BeOfType [DayOfWeek] + } - $Name = '____' + It 'will find a match from a partial name if it can' { + <# + PowerShell will match an enum value based on a partial name. + The name used must match a unique value in the enum. - $Name | Should -Not -Be 'Monday' - $Name -as [DayOfWeek] | Should -Be 'Monday' + Try making Name less than the whole word. + #> - <# - Using -as with a non-unique value will simply return $null. Casting to a non-unique - value will raise an error. - #> + $Name = '____' - $ExpectedError = '____' + $Name | Should -Not -Be 'Monday' + $Name -as [DayOfWeek] | Should -Be 'Monday' - { [DayOfWeek]'T' } | Should -Throw -ExpectedMessage $ExpectedError - } + <# + Using -as with a non-unique value will simply return $null. + Casting to a non-unique value will raise an error. + #> - It 'can retrieve all names in an enumeration with the GetEnumNames method' { - $daysOfWeek = @('____', '____', '____', '____', '____', '____', '____') + $ExpectedError = '____' - $daysOfWeek | Should -Be ([DayOfWeek].GetEnumNames()) + { [DayOfWeek]'T' } | Should -Throw -ExpectedMessage $ExpectedError + } - # The [Enum] type provides an alternative method of getting the same information. + It 'can retrieve all names in an enumeration with the GetEnumNames method' { + $daysOfWeek = @('____', '____', '____', '____', '____', '____', '____') - [Enum]::GetNames([DayOfWeek]) | Should -Be $daysOfWeek - } + $daysOfWeek | Should -Be ([DayOfWeek].GetEnumNames()) - It 'has a numeric value associated with each name' { - <# - In the DayOfWeek enumeration, Sunday has the value 0, and Monday has - the value 1, and so on. + # The [Enum] type has another method of getting the same data. - The underlying value is accessible by casting to a numeric type: + [Enum]::GetNames([DayOfWeek]) | Should -Be $daysOfWeek + } - [Int][DayOfWeek]::Sunday + It 'has a numeric value associated with each name' { + <# + In the DayOfWeek enumeration, Sunday has the value 0, and Monday + has the value 1, and so on. - Or by accessing the value__ property: + The underlying value is accessible by casting to a numeric type: - [DayOfWeek]::Sunday.value__ + [Int][DayOfWeek]::Sunday - A value from the enumeration can be compared to a number without casting or - directly accessing the value__ property. - #> + Or by accessing the value__ property: - [DayOfWeek]::Sunday | Should -Be 0 - [DayOfWeek]::____ | Should -Be 2 - } + [DayOfWeek]::Sunday.value__ - It 'can retrieve the list of possible values using the GetEnumValues method' { - <# - The values of an enumeration are displayed in much the same way as the name, - with one important difference: + A value from the enumeration can be compared to a number without + casting or directly accessing the value__ property. + #> - - GetEnumNames returns an array of strings, the names of the values only. - - GetEnumValues returns an array of values, which will be presented as the names. - #> + [DayOfWeek]::Sunday | Should -Be 0 + [DayOfWeek]::____ | Should -Be 2 + } - [DayOfWeek].GetEnumValues() | Select-Object -First 1 | Should -BeOfType [DayOfWeek] + It 'can retrieve the list of possible values using the GetEnumValues method' { + <# + The values of an enumeration are displayed in much the same way + as the name, with one important difference: + - GetEnumNames() returns an array of strings, the names of the + values only. + - GetEnumValues() returns an array of values, which will be + presented as the names. + #> - It 'has a numeric type backing each enumeration type' { - <# - Enumerations in the .NET framework, for example those created in languages such as C#, - can be of any numeric type. + [DayOfWeek].GetEnumValues() | Select-Object -First 1 | Should -BeOfType [DayOfWeek] + } - The type can be found using the GetEnumUnderlyingType method. - #> + It 'has a numeric type backing each enumeration type' { + <# + Enumerations in the .NET framework, for example those created in + languages such as C#, can be of any numeric type. - [____] | Should -Be ([DayOfWeek].GetEnumUnderlyingType()) + The type can be found using the GetEnumUnderlyingType method. + #> - # Many enums use Int32 as the underlying type. A few use other numeric types. + [____] | Should -Be ([DayOfWeek].GetEnumUnderlyingType()) - [____] | Should -Be ([System.Security.AccessControl.AceFlags].GetEnumUnderlyingType()) - } + # Most enums use Int32 as the underlying type, but not all. - It 'is created using the enum keyword' { - <# - In PowerShell enumerations are created using the enum keyword. + $underlyingType = [System.Security.AccessControl.AceFlags].GetEnumUnderlyingType() + [____] | Should -Be $underlyingType + } - All names in an enumeration have a value behind them. In some cases the value - may not be important, only the word. The simplest of enumerations is a list of - words. + It 'is created using the enum keyword' { + <# + In PowerShell enumerations are created using the enum keyword. - The following restrictions apply to the names used in an enumeration: - * Must start with A-Z, a-z, or _. - * May contain A-Z, a-z, _, and 0-9. - * Cannot use other special characters. - #> + All names in an enumeration have a value behind them. In some + cases the value may not be important, only the word. The + simplest of enumerations is a list of words. - $enumNames = Start-Job -ScriptBlock { - enum ColourOfTheRainbow { - Red - Yellow - Pink - Green - Purple - Orange - Blue - } + These restrictions apply to the names used in an enumeration: + - Must start with A-Z, a-z, or _. + - May contain A-Z, a-z, _, and 0-9. + - Cannot use other special characters. + #> - [ColourOfTheRainbow].GetEnumNames() - } | Receive-Job -Wait + $enumNames = Start-Job -ScriptBlock { + enum ColourOfTheRainbow { + Red + Yellow + Pink + Green + Purple + Orange + Blue + } - $colours = @('____', '____', '____', '____', '____', '____', '____') + [ColourOfTheRainbow].GetEnumNames() + } | Receive-Job -Wait - $enumNames | Should -Be $colours - } + $colours = @( + '____' + '____' + '____' + '____' + '____' + '____' + '____' + ) - It 'can assign specific numeric values to each enumeration value' { - <# - Each value in an enumeration can be assigned an explicit value. By default values - are automatically assigned starting from 0. + $enumNames | Should -Be $colours + } - Windows PowerShell only allows the use of Int32 values in an enumeration. - Any decimal values will be rounded to an integer. - #> + It 'can assign specific numeric values to each enumeration value' { + <# + Each value in an enumeration can be assigned an explicit value. + By default values are automatically assigned starting from 0. - $script = { - enum Number { - One = 1 - Two = 2 - } + Windows PowerShell only allows the use of Int32 values in an + enumeration. Any decimal values will be rounded to an integer. + #> - <# - Numeric values can be cast or converted to an enumeration value. + $script = { + enum Number { + One = 1 + Two = 2 + } - The $using: syntax allows access to variables from the parent PowerShell session - when using Start-Job and Invoke-Command. - #> + <# + Numeric values can be cast or converted to an enumeration + value. - $using:Value -as [Number] -as [String] - } + The $using: syntax allows access to variables from the + parent PowerShell session when using Start-Job and + Invoke-Command. + #> - $Value = 2 - $Name = Start-Job -ScriptBlock $script | Receive-Job -Wait + $using:Value -as [Number] -as [String] + } - '____' | Should -Be $Name + $Value = 2 + $Name = Start-Job -ScriptBlock $script | Receive-Job -Wait - <# - Automatic numbering may still be used. Automatic numbering continues incrementing - from the last value assigned. - #> + '____' | Should -Be $Name - $script = { - enum Number { - One = 1 - Two - Five = 5 - Six - } + <# + Automatic numbering may still be used. Automatic numbering + continues incrementing from the last value assigned. + #> - $using:Value -as [Number] -as [Int] + $script = { + enum Number { + One = 1 + Two + Five = 5 + Six } - $Value = 'Six' - $NumericValue = Start-Job -ScriptBlock $script | Receive-Job -Wait - - __ | Should -Be $NumericValue + $using:Value -as [Number] -as [Int] } - It 'can represent other integer in PowerShell Core' -Skip:($PSVersionTable.PSVersion -lt '6.2.0') { - <# - In PowerShell Core, enumerations can be based around types other than Int32. The numeric type - can be SByte, Byte, Int16, UInt16, Int32 (default), UInt32, Int64, and UInt64. + $Value = 'Six' + $NumericValue = Start-Job -ScriptBlock $script | Receive-Job -Wait - PowerShell, like C#, uses inheritance-like syntax to express the type in use. + __ | Should -Be $NumericValue + } - enum EnumName : NumericType + It 'can represent other integer in PowerShell Core' -Skip:($PSVersionTable.PSVersion -lt '6.2.0') { + <# + In PowerShell 6.2+, enumerations can be based around types other + than Int32. The numeric type can be SByte, Byte, Int16, UInt16, + Int32 (default), UInt32, Int64, and UInt64. - Inheritance is explored in the AboutClasses topic. - #> + PowerShell, like C#, uses inheritance-like syntax to express the + type in use. - $script = { - # This number is too large for Int32, Int64 is used instead. + enum EnumName : NumericType - enum Int64Enum : Int64 { - LargeValue = 9223370000000000000 - } + Inheritance is explored in the AboutClasses topic. + #> - [Int64Enum]::LargeValue -as [Int64Enum].GetEnumUnderlyingType() + $script = ' + # This number is too large for Int32, Int64 is used instead. + + enum Int64Enum : Int64 { + LargeValue = 9223370000000000000 } - $ExpectedType = [____] + [Int64Enum]::LargeValue -as [Int64Enum].GetEnumUnderlyingType() + ' - Start-Job -ScriptBlock $script | Receive-Job -Wait | Should -BeOfType $ExpectedType + $ExpectedType = [____] - # Smaller types such as SByte, Byte, Int16, and UInt16 can be used as well. - } + Start-Job -ScriptBlock [scriptblock]::Create($script) | + Receive-Job -Wait | + Should -BeOfType $ExpectedType - It 'does not require explicit casting to compare values' { - <# - In PowerShell the value on the right hand side of a comparison operator is coerced into the type - of the value on the left hand side of an operator. - #> + # Smaller types such as Byte or Int16 can also be used. + } - $script = { - enum Number { - One = 1 - Two = 2 - } + It 'does not require explicit casting to compare values' { + <# + In PowerShell the value on the right hand side of a comparison + operator is coerced into the type of the value on the left hand + side of an operator. + #> - [Number]1 -eq $using:Name + $script = { + enum Number { + One = 1 + Two = 2 } - $Name = '____' - Start-Job -ScriptBlock $script | Receive-Job -Wait | Should -BeTrue + [Number]1 -eq $using:Name } - It 'can use the enum type to test if a value exists in the enum' { - <# - The Enum type includes two static methods: + $Name = '____' + Start-Job -ScriptBlock $script | Receive-Job -Wait | Should -BeTrue + } - Parse - TryParse + It 'can use the enum type to test if a value exists in the enum' { + <# + The Enum type includes two static methods: - Parse is normally used to convert a string into an enum value. Parse is not required - in PowerShell as values can be directly cast to the enum type as shown in the first example. + Parse + TryParse - TryParse returns true if a value was successfully parsed, and false otherwise. TryParse - expects three arguments: + Parse is normally used to convert a string into an enum value. + Parse is not required in PowerShell as values can be directly + cast to the enum type as shown in the first example. - 1. The enum type. - 2. The string value to parse. - 3. A reference to a variable which can hold the parse result. + TryParse returns true if a value was successfully parsed, and + false otherwise. TryParse expects three arguments: - Running [Enum]::TryParse without parentheses will show the arguments the method expects. - #> + 1. The enum type. + 2. The string value to parse. + 3. A reference to a variable which can hold the parse + result. + + Running [Enum]::TryParse without parentheses will show the + arguments the method expects. + #> - $valueToParse = '____' - $enumType = [DayOfWeek] - $parseResult = [DayOfWeek]::Sunday + $valueToParse = '____' + $enumType = [DayOfWeek] + $parseResult = [DayOfWeek]::Sunday - $result = [Enum]::TryParse($enumType, $valueToParse, [Ref]$parseResult) + $result = [Enum]::TryParse($enumType, $valueToParse, [Ref]$parseResult) - $result | Should -BeTrue - $parseResult | Should -Not -Be 'Sunday' - } + $result | Should -BeTrue + $parseResult | Should -Not -Be 'Sunday' } + } - Context 'About Flags' { - <# - The flags attribute can be used to indicate that an enumeration represents a series of - flags. - - This allows more than one value in the enumeration to be used at once. + Context 'About Flags' { + <# + The flags attribute can be used to indicate that an enumeration + represents a series of flags. - .NET makes extensive use of flags based enumerations, from Internet security protocols, - to file system rights. - #> + This allows more than one value in the enumeration to be used at + once. - It 'represents each flag value with a single bit' { - <# - A name in the enumeration normally represents a single bit in a numeric value. + .NET makes extensive use of flags based enumerations, from Internet + security protocols, to file system rights. + #> - PowerShell enumerations use Int32 by default. PowerShell Core can use other integer types as - demonstrated in the previous context. + It 'represents each flag value with a single bit' { + <# + A name in the enumeration normally represents a single bit in a + numeric value. - 32 unique flags can be expressed in an enumeration based on Int32. Each value will be double that - of the last. Therefore expected values are 1, 2, 4, 8, 16, and so on. These are the values of - individual bits. + PowerShell enumerations use Int32 by default. PowerShell Core + can use other integer types as demonstrated in the previous + context. - The [Flags()] attribute is placed above the enum keyword. + 32 unique flags can be expressed in an enumeration based on + Int32. Each value will be double that of the last. Therefore + expected values are 1, 2, 4, 8, 16, and so on. These are the + values of individual bits. - The example below is based on 4 different flags. - #> + The [Flags()] attribute is placed above the enum keyword. - $script = { - [Flags()] - enum Bit { - None = 0 - Bit1 = 1 - Bit2 = 2 - Bit3 = 4 - Bit4 = 8 - } + The example below is based on 4 different flags. + #> - $using:Value -as [Bit] -as [String] + $script = { + [Flags()] + enum Bit { + None = 0 + Bit1 = 1 + Bit2 = 2 + Bit3 = 4 + Bit4 = 8 } - $Value = 2 - $Name = Start-Job -ScriptBlock $script | Receive-Job -Wait + $using:Value -as [Bit] -as [String] + } - '____' | Should -Be $Name + $Value = 2 + $Name = Start-Job -ScriptBlock $script | Receive-Job -Wait - # When more than one flag is described, the value returned is a comma separated list. + '____' | Should -Be $Name - $Value = 2 -bor 8 # 10 - $Name = Start-Job -ScriptBlock $script | Receive-Job -Wait + <# + If more than one flag is present, the value returned is a + comma-separated list. + #> - '____' | Should -Be $Name - } + $Value = 2 -bor 8 # 10 + $Name = Start-Job -ScriptBlock $script | Receive-Job -Wait - It 'can represent multiple flag names with a single value' { - # A list of names can be converted to a single flag value. - - $script = { - [Flags()] - enum Bit { - None = 0 - Bit1 = 1 - Bit2 = 2 - Bit3 = 4 - Bit4 = 8 - } + '____' | Should -Be $Name + } - $using:Names -as [Bit] -as [Int32] + It 'can represent multiple flag names with a single value' { + # A list of names can be converted to a single flag value. + + $script = { + [Flags()] + enum Bit { + None = 0 + Bit1 = 1 + Bit2 = 2 + Bit3 = 4 + Bit4 = 8 } - <# - The names are a single string with commas separating values, not an array. - Spaces are discarded when converting the value. - #> - - $Names = '____, ____' - $Value = Start-Job -ScriptBlock $script | Receive-Job -Wait - - __ | Should -Be $Value + $using:Names -as [Bit] -as [Int32] } - It 'allows you to test for the presence of a given flag' { - <# - A value which has multiple flags set is provided as a single value. + <# + The names are a single string with commas separating values, + not an array. Spaces are discarded when converting the value. + #> - The presence of individual flag can be tested in either of two ways. + $Names = '____, ____' + $Value = Start-Job -ScriptBlock $script | Receive-Job -Wait - One way is to call the HasFlag() method, which may be called on an enumeration value: - #> + __ | Should -Be $Value + } - $script = { - [Flags()] - enum Bit { - None = 0 - Bit1 = 1 - Bit2 = 2 - Bit3 = 4 - Bit4 = 8 - } + It 'allows you to test for the presence of a given flag' { + <# + A value which has multiple flags set is provided as a single + value. - $BitValue = [Bit]$using:Value + The presence of individual flag can be tested in either of two + ways. - $BitValue.HasFlag([Bit]$using:FlagName) + One way is to call the HasFlag() method, which may be called on + an enumeration value: + #> + + $script = { + [Flags()] + enum Bit { + None = 0 + Bit1 = 1 + Bit2 = 2 + Bit3 = 4 + Bit4 = 8 } - $Value = 10 - $FlagName = '____' + $BitValue = [Bit]$using:Value - Start-Job -ScriptBlock $script | Receive-Job -Wait | Should -BeTrue + $BitValue.HasFlag([Bit]$using:FlagName) + } - <# - The bitwise operator, -band, can also be used to test for the presence of specific flags as an - alternative to the HasFlag method. - #> + $Value = 10 + $FlagName = '____' - $script = { - [Flags()] - enum Bit { - None = 0 - Bit1 = 1 - Bit2 = 2 - Bit3 = 4 - Bit4 = 8 - } + Start-Job -ScriptBlock $script | Receive-Job -Wait | Should -BeTrue - $BitValue = [Bit]$using:Value + <# + The bitwise operator, -band, can also be used to test for the + presence of specific flags as an alternative to the HasFlag + method. + #> - # -band will return the result of the bitwise AND; either 0, or the same value as FlagName. - ($BitValue -band $using:FlagName) -eq $using:FlagName + $script = { + [Flags()] + enum Bit { + None = 0 + Bit1 = 1 + Bit2 = 2 + Bit3 = 4 + Bit4 = 8 } - $Value = 10 - $FlagName = '____, ____' + $BitValue = [Bit]$using:Value - Start-Job -ScriptBlock $script | Receive-Job -Wait | Should -BeTrue + <# + -band will return the result of the bitwise AND; either 0, + or the same value as FlagName. + #> + ($BitValue -band $using:FlagName) -eq $using:FlagName } - It 'can use values representing a combination of flags' { - <# - The .NET enumeration System.Security.AccessControl.FileSystemRights is a Flags-based - enumeration used to represent NTFS access rights. + $Value = 10 + $FlagName = '____, ____' - The enumeration contains several values which are composites of several different flags. - This technique is used to simplify the set of flags, making it easier to understand and use. + Start-Job -ScriptBlock $script | Receive-Job -Wait | Should -BeTrue + } - The enumeration below contains two different composite values. - #> + It 'can use values representing a combination of flags' { + <# + The .NET type System.Security.AccessControl.FileSystemRights + is a Flags-based enumeration used to represent NTFS access + rights. - $script = { - [Flags()] - enum ObjectType { - User = 1 - Group = 2 - UserAndGroup = 3 - Computer = 4 - } + The enumeration contains several values which are composites of + several different flags. This technique is used to simplify the + set of flags, making it easier to understand and use. - $individualValues = $using:Names -as [ObjectType] - $compositeValue = $using:CompositeName -as [ObjectType] + The enumeration below contains two different composite values. + #> - $individualValues -eq $compositeValue + $script = { + [Flags()] + enum ObjectType { + User = 1 + Group = 2 + UserAndGroup = 3 + Computer = 4 } - $CompositeName = '____' - $Names = 'User, Group' + $individualValues = $using:Names -as [ObjectType] + $compositeValue = $using:CompositeName -as [ObjectType] - Start-Job -ScriptBlock $script | Receive-Job -Wait | Should -BeTrue + $individualValues -eq $compositeValue + } - # Individual values that match a composite are automatically replaced with the name of the composite value. + $CompositeName = '____' + $Names = 'User, Group' - $script = { - [Flags()] - enum ObjectType { - User = 1 - Group = 2 - UserAndGroup = 3 - Computer = 4 - } + Start-Job -ScriptBlock $script | Receive-Job -Wait | Should -BeTrue - $using:Names -as [ObjectType] -as [String] - } + <# + Individual values that match a composite are automatically + replaced with the name of the composite value. + #> - $CompositeName = '____, ____' - $Names = 'User, Group, Computer' + $script = { + [Flags()] + enum ObjectType { + User = 1 + Group = 2 + UserAndGroup = 3 + Computer = 4 + } - Start-Job -ScriptBlock $script | Receive-Job -Wait | Should -Be $CompositeName + $using:Names -as [ObjectType] -as [String] } - } - Context 'Enumerations and Parameters' { - <# - An enumeration can be used to define the type for a parameter. This can be used as an - alternative to ValidateSet in some cases. - #> + $CompositeName = '____, ____' + $Names = 'User, Group, Computer' - It 'can be used instead of ValidateSet for a parameter' { - <# - Using the enumeration as the parameter type will offer tab-completion - to anyone using the function. - #> + Start-Job -ScriptBlock $script | + Receive-Job -Wait | + Should -Be $CompositeName + } + } - $TypeName = Start-Job -ScriptBlock { - enum ObjectType { - User - Group - } + Context 'Enumerations and Parameters' { + <# + An enumeration can be used to define the type for a parameter. This + can be used as an alternative to ValidateSet in some cases. + #> - function Get-Object { - [CmdletBinding()] - param ( - [ObjectType] - $Type - ) + It 'can be used instead of ValidateSet for a parameter' { + <# + Using the enumeration as the parameter type will offer + tab-completion to anyone using the function. + #> - $Type -as [String] - } + $TypeName = Start-Job -ScriptBlock { + enum ObjectType { + User + Group + } - Get-Object -Type User - } | Receive-Job -Wait + function Get-Object { + [CmdletBinding()] + param ( + [ObjectType] + $Type + ) - '____' | Should -Be $TypeName - } + $Type -as [String] + } - It 'will raise an error if the parameter value is incorrect' { - # If an invalid value is supplied, an error will be raised stating the permitted values. + Get-Object -Type User + } | Receive-Job -Wait - $script = { - enum ObjectType { - User - Group - } + '____' | Should -Be $TypeName + } - function Get-Object { - [CmdletBinding()] - param ( - [ObjectType]$Type - ) + It 'will raise an error if the parameter value is incorrect' { + <# + If an invalid value is supplied, an error will be raised, which + lists the permitted values. + #> + $script = { + enum ObjectType { + User + Group + } - $Type -as [String] - } + function Get-Object { + [CmdletBinding()] + param ( + [ObjectType]$Type + ) - Get-Object -Type Computer + $Type -as [String] } - # The error message is long, a partial match is enough. - $ExpectedError = '____' - - { Start-Job -ScriptBlock $script | Receive-Job -Wait -ErrorAction Stop } | - Should -Throw -ExpectedMessage $ExpectedError + Get-Object -Type Computer } - } - Context 'PowerShell Enumeration Scope' { + # The error message is long, a partial match is enough. + $ExpectedError = '____' - It 'creates PowerShell enumerations in the local scope' { - <# - Classes and enumerations are resolvable in the scope they are created and child scopes. They - are not available in parent scopes by default. - #> + { Start-Job -ScriptBlock $script | Receive-Job -Wait -ErrorAction Stop } | + Should -Throw -ExpectedMessage $ExpectedError + } + } - $Values = Start-Job -ScriptBlock { - enum ObjectType { - User - Group - } + Context 'PowerShell Enumeration Scope' { - # The enumeration can be used in the current scope + It 'creates PowerShell enumerations in the local scope' { + <# + Classes and enumerations are resolvable in the scope they are + created and child scopes. They are not available in parent + scopes by default. + #> - 'User' -as [ObjectType] -as [String] + $Values = Start-Job -ScriptBlock { + enum ObjectType { + User + Group + } - function Get-Object { - # The enumeration can be used in child scopes + # The enumeration can be used in the current scope - 'Group' -as [ObjectType] -as [String] - } + 'User' -as [ObjectType] -as [String] - Get-Object - } | Receive-Job -Wait + function Get-Object { + # The enumeration can be used in child scopes - @('____', '____') | Should -Be $Values - } + 'Group' -as [ObjectType] -as [String] + } - It 'cannot access enumerations created in child scopes' { - $script = { - function New-Enumeration { - enum ObjectType { - User - Group - } - } + Get-Object + } | Receive-Job -Wait - # The enum created in the function is not available in the parent scope. + @('____', '____') | Should -Be $Values + } - New-Enumeration - [ObjectType]::User + It 'cannot access enumerations created in child scopes' { + $script = { + function New-Enumeration { + enum ObjectType { + User + Group + } } - $ErrorMessage = '____' + # The enum defined in the function is not available outside it. - { Start-Job -ScriptBlock $script | Receive-Job -Wait -ErrorAction Stop } | - Should -Throw $ErrorMessage + New-Enumeration + [ObjectType]::User } - It 'can import enumerations with using module' { - <# - The using module statement can be used at the top of a script to make enumerations - and classes within a module available in a parent scope. - #> + $ErrorMessage = '____' - $Value = Start-Job -ScriptBlock { - $modulePath = Join-Path -Path $using:TestDrive -ChildPath 'EnumModule.psm1' + { Start-Job -ScriptBlock $script | Receive-Job -Wait -ErrorAction Stop } | + Should -Throw $ErrorMessage + } - Set-Content -Path $modulePath -Value @' + It 'can import enumerations with using module' { + <# + The using module statement can be used at the top of a script to + make enumerations and classes within a module available in a + parent scope. + #> + + $Value = Start-Job -ScriptBlock { + $modulePath = Join-Path -Path $using:TestDrive -ChildPath 'EnumModule.psm1' + + Set-Content -Path $modulePath -Value @' enum ObjectType { User Group } '@ - <# - Creating a script block avoids a complaint about how using statements must appear - first in a script for this example. + <# + Creating a script block avoids a complaint about how using + statements must appear first in a script for this example. - Ordinarily the path to, or name of, the module would be a fixed value. - #> + Ordinarily the path to, or name of, the module would be a + fixed value. + #> - & ([ScriptBlock]::Create(" + & ([ScriptBlock]::Create(" using module $modulePath 'User' -as [ObjectType] -as [String] ")) - } | Receive-Job -Wait + } | Receive-Job -Wait - '____' | Should -Be $Value - } + '____' | Should -Be $Value } } } diff --git a/Tests/KoanValidation.Tests.ps1 b/Tests/KoanValidation.Tests.ps1 index bad8dc0d0..76708e75b 100644 --- a/Tests/KoanValidation.Tests.ps1 +++ b/Tests/KoanValidation.Tests.ps1 @@ -1,62 +1,122 @@ -using module ..\PSKoans\PSKoans.psd1 +#Requires -Modules PSKoans + +using namespace System.Management.Automation.Language +using namespace System.Collections.Generic $ProjectRoot = Resolve-Path "$PSScriptRoot/.." -$KoanFolder = $ProjectRoot | Join-Path -ChildPath 'PSKoans' | Join-Path -ChildPath 'Koans' - -Describe "Koan Assessment" { - BeforeAll { - # TestCases are splatted to the script so we need hashtables - $TestCases = InModuleScope PSKoans { - Get-ChildItem -Path $KoanFolder -Recurse -Filter '*.Koans.ps1' | ForEach-Object { - $commandInfo = Get-Command -Name $_.FullName -ErrorAction SilentlyContinue - $koanAttribute = $commandInfo.ScriptBlock.Attributes.Where{ $_.TypeID -match 'Koan' } - - @{ - File = $_ - Position = $koanAttribute.Position - Module = $koanAttribute.Module +$KoanFolder = $ProjectRoot | + Join-Path -ChildPath 'PSKoans' -AdditionalChildPath 'Koans' + +Describe 'Koan Topics Static Analysis Checks' { + + Context 'Individual Topics' { + + BeforeAll { + # TestCases are splatted to the script so we need hashtables + $KoanTopics = Get-ChildItem -Path $KoanFolder -Recurse -Filter '*.Koans.ps1' | + ForEach-Object { + $commandInfo = Get-Command -Name $_.FullName -ErrorAction SilentlyContinue + $koanAttribute = $commandInfo.ScriptBlock.Attributes.Where{ $_.TypeID -match 'Koan' } + + @{ + File = $_ + Name = $_.BaseName -replace '\.Koans$' + Position = $koanAttribute.Position + Module = $koanAttribute.Module + } } - } } - } - It " koans should be valid powershell" -TestCases $TestCases { - param($File) + It 'Koan Topic should be valid powershell' -TestCases $KoanTopics { + param($File) - $File.FullName | Should -Exist + $File.FullName | Should -Exist - $Errors = $Tokens = $null - [System.Management.Automation.Language.Parser]::ParseFile($file.FullName, [ref]$Tokens, [ref]$Errors) > $null - $Errors.Count | Should -Be 0 - } + $Errors = $Tokens = $null + [Parser]::ParseFile($file.FullName, [ref]$Tokens, [ref]$Errors) > $null + $Errors.Count | Should -Be 0 + } - It " should include one (and only one) line feed at end of file" -TestCases $TestCases { - param($File) + It 'Koan Topic should not have nested It blocks' -TestCases $KoanTopics { + param($File) - $crlf = [Regex]::Match(($File | Get-Content -Raw), '(\r?(?\n))+\Z') - $crlf.Groups['lf'].Captures.Count | Should -Be 1 - } + function Test-ItBlock { + [CmdletBinding()] + param([Ast] $element) - It " should have a Koan position" -TestCases $TestCases { - param($File, $Position) + if ($element -isnot [CommandAst]) { + return $false + } - $Position | Should -Not -BeNullOrEmpty - $Position | Should -BeGreaterThan 0 - } + $commandName = $element.GetCommandName() + if ([string]::IsNullOrEmpty($commandName)) { + return $false + } + + if ($commandName -ne 'it') { + return $false + } + + return $true + } - It "Should not duplicate the Koan Position" { - $DuplicatePosition = $TestCases | - ForEach-Object { [PSCustomObject]$_ } | - Group-Object { '{0}/{1}' -f $_.Module, $_.Position } | - Where-Object Count -gt 1 | - ForEach-Object { '{0}: {1}' -f $_.Name, ($_.Group.File -join ', ') } + $Errors = $Tokens = $null + $Ast = [Parser]::ParseFile($file.FullName, [ref]$Tokens, [ref]$Errors) + $ParentItBlocks = [HashSet[Ast]]::new() - $DuplicatePosition | Should -BeNullOrEmpty + $null = $Ast.FindAll( + { + param([Ast] $currentAst) + + if (-not (Test-ItBlock $currentAst)) { + return $false + } + + for ($node = $currentAst.Parent; $null -ne $node; $node = $node.Parent) { + if (Test-ItBlock $node) { + return $ParentItBlocks.Add($node) + } + } + + return $false + }, + <# searchNestedScriptBlocks: #> $true + ) + + $ParentItBlocks | Should -BeNullOrEmpty -Because 'It blocks cannot be nested' + } + + It 'Koan Topic should include one (and only one) line feed at end of file' -TestCases $KoanTopics { + param($File) + + $crlf = [Regex]::Match(($File | Get-Content -Raw), '(\r?(?\n))+\Z') + $crlf.Groups['lf'].Captures.Count | Should -Be 1 + } + + It 'Koan Topic should have a Koan position' -TestCases $KoanTopics { + param($File, $Position) + + $Position | Should -Not -BeNullOrEmpty + $Position | Should -BeGreaterThan 0 + } } - It "Should not have other PS1 files in the Koan directory" { - Get-ChildItem -Path $KoanFolder -Recurse -Filter '*.ps1' | - Where-Object BaseName -notmatch '\.Koans$' | - Should -BeNullOrEmpty + Context 'Library Cleanliness' { + + It 'should not have topics with duplicate Koan positions' { + $DuplicatePosition = $KoanTopics | + ForEach-Object { [PSCustomObject]$_ } | + Group-Object { '{0}/{1}' -f $_.Module, $_.Position } | + Where-Object Count -gt 1 | + ForEach-Object { '{0}: {1}' -f $_.Name, ($_.Group.File -join ', ') } + + $DuplicatePosition | Should -BeNullOrEmpty + } + + It 'should not have non-Koan Topic files in the Koans directory' { + Get-ChildItem -Path $KoanFolder -Recurse -Filter '*.ps1' | + Where-Object BaseName -notmatch '\.Koans$' | + Should -BeNullOrEmpty + } } }