diff --git a/Build/Invoke-ModuleTests.ps1 b/Build/Invoke-ModuleTests.ps1 index 59f352bea..59924d910 100644 --- a/Build/Invoke-ModuleTests.ps1 +++ b/Build/Invoke-ModuleTests.ps1 @@ -1,43 +1,36 @@ $Lines = '-' * 70 -$PSVersion = $PSVersionTable.PSVersion - -Write-Host $Lines -Write-Host "STATUS: Testing with PowerShell v$($PSVersionTable.PSVersion)" -Write-Host $Lines - -# Import the module and add temporary entry to PSModulePath for build/test purposes Import-Module 'PSKoans' -$Timestamp = Get-Date -Format "yyyyMMdd-hhmmss" -$TestFile = "PS${PSVersion}_${TimeStamp}_PSKoans.TestResults.xml" -$CodeCoverageFile = "PS${PSVersion}_${TimeStamp}_PSKoans.CodeCoverage.xml" - -$ModuleFolders = @( - Get-Item -Path "$env:PROJECTROOT/PSKoans" - Get-ChildItem -Path "$env:PROJECTROOT/PSKoans" -Directory -Recurse | - Where-Object { 'Koans' -notin $_.Parent.Name, $_.Parent.Parent.Name } -).FullName -join ';' +$PesterVersion = (Get-Module -Name Pester).Version +$PSVersion = $PSVersionTable.PSVersion -# Tell Azure where the test results & code coverage files will be -Write-Host "##vso[task.setvariable variable=TestResults]$TestFile" -Write-Host "##vso[task.setvariable variable=CodeCoverageFile]$CodeCoverageFile" -Write-Host "##vso[task.setvariable variable=SourceFolders]$ModuleFolders" +Write-Host $Lines +Write-Host "TEST: PowerShell Version: $PSVersion" +Write-Host "TEST: Pester Version: $PesterVersion" +Write-Host $Lines -# Gather test results. Store them in a variable and file -$PesterParams = @{ - Path = "$env:PROJECTROOT/Tests" - PassThru = $true - OutputFormat = 'NUnitXml' - OutputFile = "$env:BUILD_ARTIFACTSTAGINGDIRECTORY/$TestFile" - Show = "Header", "Failed", "Summary" - CodeCoverage = (Get-ChildItem -Recurse -Path "$env:PROJECTROOT/PSKoans" -Filter '*.ps*1' -Exclude '*.Koans.ps1').FullName - CodeCoverageOutputFile = "$env:BUILD_ARTIFACTSTAGINGDIRECTORY/$CodeCoverageFile" +try { + # Try/Finally required since -CI will exit with exit code on failure. + Invoke-Pester -Path "$env:PROJECTROOT" -CI -Output Normal } -$TestResults = Invoke-Pester @PesterParams - -# If tests failed, write errors and exit -if ($TestResults.FailedCount -gt 0) { - Write-Error "Failed $($TestResults.FailedCount) tests; build failed!" - exit $TestResults.FailedCount +finally { + $Timestamp = Get-Date -Format "yyyyMMdd-hhmmss" + $TestFile = "PS${PSVersion}_${TimeStamp}_PSKoans.TestResults.xml" + $CodeCoverageFile = "PS${PSVersion}_${TimeStamp}_PSKoans.CodeCoverage.xml" + + $ModuleFolders = @( + Get-Item -Path "$env:PROJECTROOT/PSKoans" + Get-ChildItem -Path "$env:PROJECTROOT/PSKoans" -Directory -Recurse | + Where-Object FullName -NotMatch '[\\/]Tests[\\/]|[\\/]PSKoans[\\/]Koans[\\/]' + ).FullName -join ';' + + # Tell Azure what the test results & code coverage file names will be + Write-Host "##vso[task.setvariable variable=TestResults]$TestFile" + Write-Host "##vso[task.setvariable variable=CodeCoverageFile]$CodeCoverageFile" + Write-Host "##vso[task.setvariable variable=SourceFolders]$ModuleFolders" + + # Move files generated from Invoke-Pester to expected location + Move-Item -Path './testResults.xml' -Destination "$env:BUILD_ARTIFACTSTAGINGDIRECTORY/$TestFile" + Move-Item -Path './coverage.xml' -Destination "$env:BUILD_ARTIFACTSTAGINGDIRECTORY/$CodeCoverageFile" } diff --git a/KoanIndex.md b/KoanIndex.md index 7f5c7972d..bd1d5314d 100644 --- a/KoanIndex.md +++ b/KoanIndex.md @@ -60,13 +60,18 @@ When adding or updating koan topics, please keep this file up to date. ### ActiveDirectory -| Folder | Topic | Position | -| ----------------------- | :------------------------------------------------------------- | :------: | -| Introduction | [AboutFiltering][ActiveDirectory\AboutFiltering] | 101 | +| Folder | Topic | Position | +| ------------ | :----------------------------------------------- | :------: | +| Introduction | [AboutFiltering][ActiveDirectory\AboutFiltering] | 101 | ### Dbatools -> TBD +| Folder | Topic | Position | +| -------- | :-------------------------------------------------------- | :------: | +| dbatools | [AboutDbaDatabase][dbatools\AboutDbaDatabase] | 1001 | +| dbatools | [AboutQueryingDatabases][dbatools\AboutQueryingDatabases] | 1002 | +| dbatools | [AboutNewDatabases][dbatools\AboutNewDatabases] | 1003 | +| dbatools | [AboutBackupDatabases][dbatools\AboutBackupDatabases] | 1004 | @@ -120,3 +125,8 @@ When adding or updating koan topics, please keep this file up to date. [ActiveDirectory\AboutFiltering]: PSKoans/Koans/Modules/ActiveDirectory/Introduction/AboutFiltering.Koans.ps1 + +[dbatools\AboutDbaDatabase]: PSKoans/Koans/Modules/dbatools/AboutDbaDatabase.Koans.ps1 +[dbatools\AboutQueryingDatabases]: PSKoans/Koans/Modules/dbatools/AboutQueryingDatabases.Koans.ps1 +[dbatools\AboutNewDatabases]: PSKoans/Koans/Modules/dbatools/AboutNewDatabases.Koans.ps1 +[dbatools\AboutBackupDatabases]: PSKoans/Koans/Modules/dbatools/AboutBackupDatabases.Koans.ps1 diff --git a/PSKoans.ezformat.ps1 b/PSKoans.ezformat.ps1 index e4168bdce..dff285117 100644 --- a/PSKoans.ezformat.ps1 +++ b/PSKoans.ezformat.ps1 @@ -1,38 +1,37 @@ -#Requires -Modules EZOut -# Install-Module EZOut -# or https://github.com/StartAutomating/EZOut - -$VerbosePreference = 'Continue' - -$ModuleName = $MyInvocation.MyCommand.Name -replace '\.ezformat\.ps1', '' -$ModuleFolder = Get-Item -Path "$PSScriptRoot/PSKoans" | Select-Object -ExpandProperty FullName - -Write-Verbose "Building format file for '$ModuleName' in '$ModuleFolder'" - +#requires -Module EZOut +# Install-Module EZOut or https://github.com/StartAutomating/EZOut +$myFile = $MyInvocation.MyCommand.ScriptBlock.File +$myModuleName = 'PSKoans' +$myRoot = "$PSScriptRoot" +Push-Location $myRoot $formatting = @( + # Add your own Write-FormatView here, + # or put them in a Formatting or Views directory foreach ($potentialDirectory in 'formatting', 'views') { - $path = Join-Path $PSScriptRoot -ChildPath $potentialDirectory - if (Test-Path $path) { - Get-ChildItem -Path $path | ForEach-Object { - Write-Verbose "Processing file '$($_.FullName)'" - Import-FormatView -FilePath $_.FullName - } - } + Join-Path $myRoot -ChildPath $potentialDirectory | + Get-ChildItem -ea ignore | + Import-FormatView -FilePath { $_.Fullname } } ) +$destinationRoot = "$PSScriptRoot/PSKoans" + if ($formatting) { - $formatFilePath = Join-Path $ModuleFolder -ChildPath "$ModuleName.format.ps1xml" - Write-Verbose "Writing format file to '$formatFilePath'" - $formatting | Out-FormatData -Module $ModuleName | Set-Content $formatFilePath -Encoding UTF8 + $myFormatFile = Join-Path $destinationRoot "$myModuleName.format.ps1xml" + $formatting | Out-FormatData -Module $MyModuleName | Set-Content $myFormatFile -Encoding UTF8 } $types = @( # Add your own Write-TypeView statements here + # or declare them in the 'Types' directory + Join-Path $myRoot Types | + Get-Item -ea ignore | + Import-TypeView + ) if ($types) { - $TypesFile = Join-Path $PSScriptRoot "$ModuleName.types.ps1xml" - Write-Verbose "Writing types file to '$TypesFile'" - $types | Out-TypeData | Set-Content $TypesFile -Encoding UTF8 + $myTypesFile = Join-Path $destinationRoot "$myModuleName.types.ps1xml" + $types | Out-TypeData | Set-Content $myTypesFile -Encoding UTF8 } +Pop-Location diff --git a/PSKoans/Koans/Cmdlets 2/AboutCsvCmdlets.Koans.ps1 b/PSKoans/Koans/Cmdlets 2/AboutCsvCmdlets.Koans.ps1 index a5ad4f0ba..1a04cbe6f 100644 --- a/PSKoans/Koans/Cmdlets 2/AboutCsvCmdlets.Koans.ps1 +++ b/PSKoans/Koans/Cmdlets 2/AboutCsvCmdlets.Koans.ps1 @@ -19,7 +19,7 @@ param() handled directly by PowerShell's CSV cmdlets without some extra work. #> -Describe '*-Csv Cmdlets' { +Describe 'CSV Cmdlets' { Context 'Export-Csv' { <# @@ -81,7 +81,21 @@ Describe '*-Csv Cmdlets' { } It 'allows you to specify the delimiter' { + $Delimiter = '_' + $Path = 'TestDrive:/DelimitedData.csv' + + $Text = @( + '"Number","Square"' + '"__"?"__"' + '"__"?"__"' + '"__"?"__"' + '"__"?"__"' + '"__"?"__"' + ) + $Objects | Export-Csv -Path $Path -Delimiter $Delimiter + $FileContents = Get-Content -Path $Path + $Text | Should -BeExactly $FileContents } } @@ -207,6 +221,7 @@ Describe '*-Csv Cmdlets' { } Context 'ConvertFrom-Csv' { + BeforeAll { $CsvString = @" "Number","Square" diff --git a/PSKoans/Koans/Cmdlets 2/AboutOutCmdlets.Koans.ps1 b/PSKoans/Koans/Cmdlets 2/AboutOutCmdlets.Koans.ps1 index 6caec8752..e47471d89 100644 --- a/PSKoans/Koans/Cmdlets 2/AboutOutCmdlets.Koans.ps1 +++ b/PSKoans/Koans/Cmdlets 2/AboutOutCmdlets.Koans.ps1 @@ -77,7 +77,6 @@ Context 'Out-* Cmdlets' { partially because in older versions of PowerShell it has a different default encoding than those cmdlets, frequently confusing users. #> - It 'stores data in files' { $Path = 'TestDrive:\File.txt' diff --git a/PSKoans/Koans/Constructs and Patterns/AboutSplatting.Koans.ps1 b/PSKoans/Koans/Constructs and Patterns/AboutSplatting.Koans.ps1 index 2c13bdcac..cf4a500b8 100644 --- a/PSKoans/Koans/Constructs and Patterns/AboutSplatting.Koans.ps1 +++ b/PSKoans/Koans/Constructs and Patterns/AboutSplatting.Koans.ps1 @@ -15,6 +15,7 @@ param() 'splatted' into the command by specifying the variable name with an @ symbol: @Variable #> Describe 'Splatting' { + BeforeAll { $PSKoansFolder = Get-PSKoanLocation } @@ -74,8 +75,8 @@ Describe 'Splatting' { } Get-ChildItem @Parameters | - Select-Object -First 1 | - Should -BeOfType 'System.IO.FileInfo' + Select-Object -First 1 | + Should -BeOfType 'System.IO.FileInfo' } It 'can be built from automatic hashtables' { diff --git a/PSKoans/Koans/Constructs and Patterns/AboutStringBuilder.Koans.ps1 b/PSKoans/Koans/Constructs and Patterns/AboutStringBuilder.Koans.ps1 index 0592ee43a..48eb522af 100644 --- a/PSKoans/Koans/Constructs and Patterns/AboutStringBuilder.Koans.ps1 +++ b/PSKoans/Koans/Constructs and Patterns/AboutStringBuilder.Koans.ps1 @@ -96,6 +96,7 @@ Describe 'System.Text.StringBuilder' { } Context 'Constructing the Final String Object' { + BeforeAll { $SB = [System.Text.StringBuilder]::new("Hello!") $SB.Append("BYE!") @@ -115,9 +116,11 @@ Describe 'System.Text.StringBuilder' { } Context 'Other StringBuilder Methods' { + BeforeAll { $StringBuilder = [System.Text.StringBuilder]::new("TEXT") } + It 'can be cleared' { $StringBuilder.Clear() $StringBuilder.Length | Should -Be 0 @@ -143,6 +146,7 @@ Describe 'System.Text.StringBuilder' { } Context 'StringBuilder Properties' { + BeforeAll { $StringBuilder = [System.Text.StringBuilder]::new() $StringBuilder.AppendLine('When you look into the void,') @@ -154,12 +158,12 @@ Describe 'System.Text.StringBuilder' { $PropertyName = '____' $Properties = $StringBuilder | - Get-Member | - Where-Object MemberType -eq 'Property' + Get-Member | + Where-Object MemberType -EQ 'Property' $ExpectedPropertyCount = $Properties | - Measure-Object | - Select-Object -ExpandProperty Count + Measure-Object | + Select-Object -ExpandProperty Count $PropertyCount | Should -Be $ExpectedPropertyCount $PropertyName | Should -BeIn $Properties.Name diff --git a/PSKoans/Koans/Foundations/AboutAssignmentAndArithmetic.Koans.ps1 b/PSKoans/Koans/Foundations/AboutAssignmentAndArithmetic.Koans.ps1 index 09cf6f805..6484d492f 100644 --- a/PSKoans/Koans/Foundations/AboutAssignmentAndArithmetic.Koans.ps1 +++ b/PSKoans/Koans/Foundations/AboutAssignmentAndArithmetic.Koans.ps1 @@ -98,6 +98,7 @@ Describe 'Arithmetic Operators' { __ | Should -Be $Array } } + Context 'Subtraction' { It 'works similarly to addition' { @@ -148,9 +149,10 @@ Describe 'Arithmetic Operators' { } Context 'Modulus' { - - # Modulus is a bit of an odd one, but common enough in programming. It performs a - # division, and then returns the integer value of the remainder. + <# + Modulus is a bit of an odd one, but common enough in programming. It performs a + division, and then returns the integer value of the remainder. + #> It 'is usually used with integers' { $Remainder = 15 % 7 __ | Should -Be $Remainder diff --git a/PSKoans/Koans/Introduction/AboutBinary.Koans.ps1 b/PSKoans/Koans/Introduction/AboutBinary.Koans.ps1 index 3cab25d35..d6232eb87 100644 --- a/PSKoans/Koans/Introduction/AboutBinary.Koans.ps1 +++ b/PSKoans/Koans/Introduction/AboutBinary.Koans.ps1 @@ -43,108 +43,155 @@ param() one, and 1 + 2 is equal to 3. #> -Describe "Binary conversion" { +Describe 'Binary Conversions' { - It "Bit conversion" { - # What would 0 be if converted to be boolean? - # Replace __ with either $true or $false - $____ -as [int] | Should -Be 0 + Context 'Boolean Conversions' { - # What would 1 be if converted to be boolean? - # Replace __ with either $true or $false - $____ -as [int] | Should -Be 1 - } + It 'converts $false to an integer' { + $ExpectedResult = $false -as [int] - It "Binary to integer conversion" { - # Replace __ with the decimal value of 1111 - # E.G. __ becomes 1234 - $Binary = "1111" - $Value = __ - $Value | Should -Be ([Convert]::ToInt32($Binary, 2)) - - # Replace __ with the decimal value of 1000 - $Binary = "1000" - $Value = __ - $Value | Should -Be ([Convert]::ToInt32($Binary, 2)) - - # Replace __ with the decimal value of 0010 - $Binary = "0010" - $Value = __ - $Value | Should -Be ([Convert]::ToInt32($Binary, 2)) - - # Replace __ with the decimal value of 1001 - $Binary = "1001" - $Value = __ - $Value | Should -Be ([Convert]::ToInt32($Binary, 2)) - - # Replace __ with the decimal value of 11111111 - $Binary = "11111111" - $Value = __ - $Value | Should -Be ([Convert]::ToInt32($Binary, 2)) - - # Replace __ with the decimal value of 10101010 - $Binary = "10101010" - $Value = __ - $Value | Should -Be ([Convert]::ToInt32($Binary, 2)) - - # Replace __ with the decimal value of 11001100 - $Binary = "11001100" - $Value = __ - $Value | Should -Be ([Convert]::ToInt32($Binary, 2)) - - # Replace __ with the decimal value of 11110001 - $Binary = "11110001" - $Value = __ - $Value | Should -Be ([Convert]::ToInt32($Binary, 2)) + # What would $false be if converted to a number? + __ | Should -Be $ExpectedResult + } + It 'converts $true to an integer' { + $ExpectedResult = $true -as [int] + # What would $true be if converted to a number? + __ | Should -Be $ExpectedResult + } } - It "Integer to Binary conversion" { + Context "Binary to Integer Conversion" { <# - Convert the following integers into their binary representation + Replace the blanks below with the decimal value of the binary + numbers in each case. For example, the binary sequence "10" is + represented by the number 2 in the standard decimal system. #> + It 'converts 1111 to an integer' { + $ExpectedValue = [Convert]::ToInt32($Binary, 2) + + # Replace the __ with the decimal value of 1111 + $Binary = "1111" + __ | Should -Be $ExpectedValue + } + + It 'converts 1000 to an integer' { + $ExpectedValue = [Convert]::ToInt32($Binary, 2) + + # Replace __ with the decimal value of 1000 + $Binary = "1000" + __ | Should -Be $ExpectedValue + } + + It 'converts 0010 to an integer' { + $ExpectedValue = [Convert]::ToInt32($Binary, 2) + + # Replace __ with the decimal value of 0010 + $Binary = "0010" + __ | Should -Be $ExpectedValue + } + + It 'converts 1001 to an integer' { + $ExpectedValue = [Convert]::ToInt32($Binary, 2) + + # Replace __ with the decimal value of 1001 + $Binary = "1001" + __ | Should -Be $ExpectedValue + } + + It 'converts 11111111 to an integer' { + $ExpectedValue = [Convert]::ToInt32($Binary, 2) + + # Replace __ with the decimal value of 11111111 + $Binary = "11111111" + __ | Should -Be $ExpectedValue + } - # Replace __ with the binary value of 7 - # E.G. "__" becomes "0100" - $Binary = "____" - $Value = 7 - $Value | Should -Be ([Convert]::ToInt32($Binary, 2)) - - # Replace __ with the binary value of 12 - $Binary = "____" - $Value = 12 - $Value | Should -Be ([Convert]::ToInt32($Binary, 2)) - - # Replace __ with the binary value of 2 - $Binary = "____" - $Value = 2 - $Value | Should -Be ([Convert]::ToInt32($Binary, 2)) - - # Replace __ with the binary value of 14 - $Binary = "____" - $Value = 14 - $Value | Should -Be ([Convert]::ToInt32($Binary, 2)) - - # Replace __ with the binary value of 103 - # E.G. "__" becomes "01001110" - $Binary = "____" - $Value = 103 - $Value | Should -Be ([Convert]::ToInt32($Binary, 2)) - - # Replace __ with the binary value of 250 - $Binary = "____" - $Value = 250 - $Value | Should -Be ([Convert]::ToInt32($Binary, 2)) - - # Replace __ with the binary value of 74 - $Binary = "____" - $Value = 74 - $Value | Should -Be ([Convert]::ToInt32($Binary, 2)) - - # Replace __ with the binary value of 32 - $Binary = "____" - $Value = 32 - $Value | Should -Be ([Convert]::ToInt32($Binary, 2)) + It 'converts 10101010 to an integer' { + $ExpectedValue = [Convert]::ToInt32($Binary, 2) + + # Replace __ with the decimal value of 10101010 + $Binary = "10101010" + __ | Should -Be $ExpectedValue + } + + It 'converts 11001100 to an integer' { + $ExpectedValue = [Convert]::ToInt32($Binary, 2) + + # Replace __ with the decimal value of 11001100 + $Binary = "11001100" + __ | Should -Be $ExpectedValue + } + + It 'converts 11110001 to an integer' { + $ExpectedValue = [Convert]::ToInt32($Binary, 2) + + # Replace __ with the decimal value of 11110001 + $Binary = "111g10001" + __ | Should -Be $ExpectedValue + } + } + + Context "Integer to Binary Conversion" { + <# + Convert the following integers into their binary representation. + For example, 2 is represented in binary with the digits "10". + #> + It 'converts the integer 7 to binary' { + # Replace ____ with the binary value of 7 + $Value = 7 + $Binary = [Convert]::ToString($Value, 2) + '____' | Should -Be $Binary + } + + It 'converts the integer 12 to binary' { + # Replace __ with the binary value of 12 + $Value = 12 + $Binary = [Convert]::ToString($Value, 2) + '____' | Should -Be ([Convert]::ToInt32($Binary, 2)) + } + + It 'converts the integer 2 to binary' { + # Replace __ with the binary value of 2 + $Value = 2 + $Binary = [Convert]::ToString($Value, 2) + '____' | Should -Be ([Convert]::ToInt32($Binary, 2)) + } + + It 'converts the integer 14 to binary' { + # Replace __ with the binary value of 14 + $Value = 14 + $Binary = [Convert]::ToString($Value, 2) + '____' | Should -Be ([Convert]::ToInt32($Binary, 2)) + } + + It 'converts the integer 103 to binary' { + # Replace __ with the binary value of 103 + $Value = 103 + $Binary = [Convert]::ToString($Value, 2) + '____' | Should -Be ([Convert]::ToInt32($Binary, 2)) + } + + It 'converts the integer 250 to binary' { + # Replace __ with the binary value of 250 + $Value = 250 + $Binary = [Convert]::ToString($Value, 2) + '____' | Should -Be ([Convert]::ToInt32($Binary, 2)) + } + + It 'converts the integer 74 to binary' { + # Replace __ with the binary value of 74 + $Value = 74 + $Binary = [Convert]::ToString($Value, 2) + '____' | Should -Be ([Convert]::ToInt32($Binary, 2)) + } + + It 'converts the integer 32 to binary' { + # Replace __ with the binary value of 32 + $Value = 32 + $Binary = [Convert]::ToString($Value, 2) + '____' | Should -Be ([Convert]::ToInt32($Binary, 2)) + } } } diff --git a/PSKoans/Koans/Introduction/AboutBooleans.Koans.ps1 b/PSKoans/Koans/Introduction/AboutBooleans.Koans.ps1 index 5b0418a87..a0f0f8f16 100644 --- a/PSKoans/Koans/Introduction/AboutBooleans.Koans.ps1 +++ b/PSKoans/Koans/Introduction/AboutBooleans.Koans.ps1 @@ -8,7 +8,7 @@ param() allow us to represent a logical, binary, state. Effectively, they represent the state of a single bit of data. For example: - | "Can I stop this service?" + "Can I stop this service?" The answer is yes or no, True or False. @@ -27,30 +27,42 @@ param() #> Describe "Booleans" { + <# + Fill in the blanks below with either $true or $false, indicating the + result you expect from the noted expressions. - # Using only booleans, either $true or $false, fill in the blanks below. + We'll cover comparison operators in more detail later on, but here's a + summary of the operators used here: - It '( 1 -gt 2 ) is either true or false' { + -gt => "is greater than" + -eq => "is equal to" + -lt => "is less than" + + As an example, the expression 7 -gt 5 asserts "7 is greater than 5". + This is a true statement, so the final value of the expression is $true. + #> + + It 'evaluates ( 1 -gt 2 ) as a boolean expression' { $____ | Should -Be ( 1 -gt 2 ) -Because '1 is not greater than 2' } - It '( 1 -lt 2 ) is either true or false' { + It 'evaluates ( 1 -lt 2 ) as a boolean expression' { $____ | Should -Be ( 1 -lt 2 ) -Because '1 is less than 2' } - It '( 10 -lt 20 ) is either true or false' { + It 'evaluates ( 10 -lt 20 ) as a boolean expression' { $____ | Should -Be ( 10 -lt 20 ) -Because '10 is less than 20' } - It '( 10 -gt 20 ) is either true or false' { + It 'evaluates ( 10 -gt 20 ) as a boolean expression' { $____ | Should -Be ( 10 -gt 20 ) -Because 'The lesser is not greater' } - It '( 3 -eq 3 ) is either true or false' { + It 'evaluates ( 3 -eq 3 ) as a boolean expression' { $____ | Should -Be ( 3 -eq 3 ) -Because 'A mirror reflects true' } - It '( 100 -lt 1 ) is either true or false' { + It 'evaluates ( 100 -lt 1 ) as a boolean expression' { $____ | Should -Be ( 100 -lt 1 ) -Because '100 is not less than 1' } } diff --git a/PSKoans/Koans/Introduction/AboutGetMember.Koans.ps1 b/PSKoans/Koans/Introduction/AboutGetMember.Koans.ps1 index 15952e115..557e0c3af 100644 --- a/PSKoans/Koans/Introduction/AboutGetMember.Koans.ps1 +++ b/PSKoans/Koans/Introduction/AboutGetMember.Koans.ps1 @@ -38,95 +38,129 @@ param() #> Describe "Get Member" { -<# - Use Get-Member to identify properties or methods of some common objects! - - Make sure you use a different cmdlet each time, we'll be checking :) -#> - - It 'allows us to explore properties on an object' { - - # Get some properties! - <# EXAMPLE + Context 'Exploring Object Properties' { + <# + Let's look at some object properties! - $Cmdlet1 = 'Get-Process' - $PropertyName = 'Threads' - $Reason = $BecauseString -f $PropertyName, $cmdlet1 - & (Get-Command -Name $Cmdlet1) | - Get-Member -MemberType Property -Name $PropertyName | - Should -Not -BeNullOrEmpty -Because $Reason + Using any three cmdlets you like (make sure you use three different + cmdlets!), use Get-Member in your console to peek at the properties + on the object. - #> - $BecauseString = "property '{0}' should be present in output from {1}" - - $Cmdlet1 = '____' - $PropertyName = '____' - - $Reason = $BecauseString -f $PropertyName, $cmdlet1 - & (Get-Command -Name $Cmdlet1) | - Get-Member -MemberType Property -Name $PropertyName | - Should -Not -BeNullOrEmpty -Because $Reason + Let's see an example; by sending the output from Get-Process into + Get-Member we can inspect the objects Get-Process outputs. You can + run the below command in your console: - $Cmdlet2 = '____' - $PropertyName = '____' + Get-Process | Get-Member -MemberType Property - $Reason = $BecauseString -f $PropertyName, $cmdlet2 - & (Get-Command -Name $Cmdlet2) | - Get-Member -MemberType Property -Name $PropertyName | - Should -Not -BeNullOrEmpty -Because $Reason + From the output of the above command, we will see that one of the + properties is named "Threads", so using that cmdlet name and that + property name will satisfy one of the tests below. - $Cmdlet3 = '____' - $PropertyName = '____' + The others are up to you! Get-* cmdlets will be most helpful here; + if you're not sure which to try, you can use the following command + to list Get-* cmdlets you can try out: - $Reason = $BecauseString -f $PropertyName, $cmdlet3 - & (Get-Command -Name $Cmdlet3) | - Get-Member -MemberType Property -Name $PropertyName | - Should -Not -BeNullOrEmpty -Because $Reason - - $cmdlet1, $cmdlet2, $cmdlet3 | - Get-Unique | - Should -HaveCount 3 -Because "three unique cmdlets should be supplied" + Get-Command -Verb Get + #> + BeforeAll { + $Cmdlets = [System.Collections.Generic.HashSet[string]]::new() + $PropertyString = "property '{0}' should be present in output from {1}" + $UniqueString = 'unique cmdlets should be used for each test' + } + + It 'lists one of the properties of the first unique command' { + $CmdletName = '____' + $PropertyName = '____' + + $Reason = $PropertyString -f $PropertyName, $CmdletName + & (Get-Command -Name $CmdletName) | + Get-Member -MemberType Property -Name $PropertyName | + Should -Not -BeNullOrEmpty -Because $Reason + + $Cmdlets.Add($CmdletName) | Should -BeTrue -Because $UniqueString + } + + It 'lists one of the properties of the second unique command' { + $CmdletName = '____' + $PropertyName = '____' + + $Reason = $PropertyString -f $PropertyName, $CmdletName + & (Get-Command -Name $CmdletName) | + Get-Member -MemberType Property -Name $PropertyName | + Should -Not -BeNullOrEmpty -Because $Reason + + $Cmdlets.Add($CmdletName) | Should -BeTrue -Because $UniqueString + } + + It 'lists one of the properties of the third unique command' { + $CmdletName = '____' + $PropertyName = '____' + + $Reason = $PropertyString -f $PropertyName, $CmdletName + & (Get-Command -Name $CmdletName) | + Get-Member -MemberType Property -Name $PropertyName | + Should -Not -BeNullOrEmpty -Because $Reason + + $Cmdlets.Add($CmdletName) | Should -BeTrue -Because $UniqueString + } } - It 'allows us to explore methods on an object' { + Context 'Exploring Object Methods' { + <# + Similar to above, you can inspect the methods available from an + object that a cmdlet outputs, by changing the -MemberType value + you provide to Get-Member: - # Get some methods! + Get-Process | Get-Member -MemberType Method - <# EXAMPLE + If you don't provide a -MemberType option and value, it will simply + list all the members, regardless of the kind of members they are. - $Cmdlet1 = 'Get-Process' - $MethodName = 'Close' - $Reason = $BecauseString -f $MethodName, $cmdlet1 - & (Get-Command -Name $Cmdlet1) | - Get-Member -MemberType Property -Name $MethodName | - Should -Not -BeNullOrEmpty -Because $Reason + You can reuse the same set of cmdlets from above here if you wish, + but you will need to check to see if there are methods available on + the objects they output! #> - $BecauseString = "method '{0}' should be present in output from {1}" - - $Cmdlet1 = '____' - $MethodName = '____' - $Reason = $BecauseString -f $MethodName, $cmdlet1 - & (Get-Command -Name $Cmdlet1) | - Get-Member -MemberType Method -Name $MethodName | - Should -Not -BeNullOrEmpty -Because $Reason - - $Cmdlet2 = '____' - $MethodName = '____' - $Reason = $BecauseString -f $MethodName, $cmdlet2 - & (Get-Command -Name $Cmdlet2) | - Get-Member -MemberType Method -Name $MethodName | - Should -Not -BeNullOrEmpty -Because $Reason - - $Cmdlet3 = '____' - $MethodName = '____' - $Reason = $BecauseString -f $MethodName, $cmdlet3 - & (Get-Command -Name $Cmdlet3) | - Get-Member -MemberType Method -Name $MethodName | - Should -Not -BeNullOrEmpty -Because $Reason - - $cmdlet1, $cmdlet2, $cmdlet3 | - Get-Unique | - Should -HaveCount 3 -Because "three unique cmdlets should be supplied" + BeforeAll { + $Cmdlets = [System.Collections.Generic.HashSet[string]]::new() + $MethodString = "property '{0}' should be present in output from {1}" + $UniqueString = 'unique cmdlets should be used for each test' + } + + It 'lists one of the methods of the first unique command' { + $CmdletName = '____' + $MethodName = '____' + + $Reason = $MethodString -f $MethodName, $CmdletName + & (Get-Command -Name $CmdletName) | + Get-Member -MemberType Property -Name $MethodName | + Should -Not -BeNullOrEmpty -Because $Reason + + $Cmdlets.Add($CmdletName) | Should -BeTrue -Because $UniqueString + } + + It 'lists one of the methods of the second unique command' { + $CmdletName = '____' + $MethodName = '____' + + $Reason = $MethodString -f $MethodName, $CmdletName + & (Get-Command -Name $CmdletName) | + Get-Member -MemberType Property -Name $MethodName | + Should -Not -BeNullOrEmpty -Because $Reason + + $Cmdlets.Add($CmdletName) | Should -BeTrue -Because $UniqueString + } + + It 'lists one of the methods of the third unique command' { + $CmdletName = '____' + $MethodName = '____' + + $Reason = $MethodString -f $MethodName, $CmdletName + & (Get-Command -Name $CmdletName) | + Get-Member -MemberType Property -Name $MethodName | + Should -Not -BeNullOrEmpty -Because $Reason + + $Cmdlets.Add($CmdletName) | Should -BeTrue -Because $UniqueString + } } } diff --git a/PSKoans/Koans/Introduction/AboutNumbers.Koans.ps1 b/PSKoans/Koans/Introduction/AboutNumbers.Koans.ps1 index a4062e5ea..8fa10cb70 100644 --- a/PSKoans/Koans/Introduction/AboutNumbers.Koans.ps1 +++ b/PSKoans/Koans/Introduction/AboutNumbers.Koans.ps1 @@ -26,17 +26,25 @@ Describe 'Basic Number Types' { Context 'Double' { - It 'can result from an operation involving multiple types of numbers' { - <# - When doing arithmetic on different types of numbers, PowerShell - will automatically convert the less precise or more narrow type - to the other kind. - #> + BeforeAll { $Int = 10 $Double = 10.0 + } + It 'has a specific object type for integers' { 'System.____' | Should -Be $Int.GetType().Fullname + } + + It 'has a specific object type for doubles' { 'System.____' | Should -Be $Double.GetType().Fullname + } + + It 'can result from mathematical operations with other number types' { + <# + When doing arithmetic on different types of numbers, PowerShell + will automatically convert the less precise or more narrow type + to the other kind. + #> $Result = $Int * $Double diff --git a/PSKoans/Koans/Katas/ProcessingStrings.Koans.ps1 b/PSKoans/Koans/Katas/ProcessingStrings.Koans.ps1 index 6a225d4cd..732c2b469 100644 --- a/PSKoans/Koans/Katas/ProcessingStrings.Koans.ps1 +++ b/PSKoans/Koans/Katas/ProcessingStrings.Koans.ps1 @@ -26,6 +26,7 @@ param() -Joel #> Describe "The Stock Challenge" { + BeforeAll { $StockData = @( "Date,Open,High,Low,Close,Volume,Adj Close" diff --git a/PSKoans/PSKoans.psd1 b/PSKoans/PSKoans.psd1 index c786f10db..df3e9e77a 100644 --- a/PSKoans/PSKoans.psd1 +++ b/PSKoans/PSKoans.psd1 @@ -54,7 +54,7 @@ RequiredModules = @( @{ ModuleName = 'Pester' - RequiredVersion = '4.10.1' + ModuleVersion = '5.0.2' } ) @@ -68,7 +68,7 @@ # TypesToProcess = @() # Format files (.ps1xml) to be loaded when importing this module - FormatsToProcess = @('PSKoans.format.ps1xml') + FormatsToProcess = @( 'PSKoans.format.ps1xml' ) # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess # NestedModules = @() @@ -86,10 +86,10 @@ 'Move-PSKoanLibrary' - 'Reset-PSKoan' - 'Register-Advice' + 'Reset-PSKoan' + 'Show-Advice' 'Show-Karma' @@ -106,13 +106,17 @@ AliasesToExport = @( 'Invoke-PSKoans' 'Test-Koans' - 'Get-Enlightenment' - 'Measure-Karma' + '__' '____' 'FILL_ME_IN' + 'Clear-Path' + 'Get-Advice' + + 'Get-Enlightenment' + 'Measure-Karma' ) # DSC resources to export from this module @@ -172,5 +176,4 @@ # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. # DefaultCommandPrefix = '' - } diff --git a/PSKoans/PSKoans.psm1 b/PSKoans/PSKoans.psm1 index b22ae8adb..a6358912b 100644 --- a/PSKoans/PSKoans.psm1 +++ b/PSKoans/PSKoans.psm1 @@ -21,6 +21,7 @@ $script:DefaultSettings = @{ } [hashtable] $script:CurrentTopic = $null +[runspace] $script:KoanRunspace = $null #region SupportingClasses diff --git a/PSKoans/Private/Assert-UnblockedFile.ps1 b/PSKoans/Private/Assert-UnblockedFile.ps1 index 899741c83..00c4913e3 100644 --- a/PSKoans/Private/Assert-UnblockedFile.ps1 +++ b/PSKoans/Private/Assert-UnblockedFile.ps1 @@ -43,7 +43,7 @@ function Assert-UnblockedFile { } } - if (Get-Content -Path $FileInfo.FullName -Stream Zone.Identifier -ErrorAction SilentlyContinue) { + if (Get-Content -Path $FileInfo.FullName -Stream Zone.Identifier -ErrorAction Ignore) { $ErrorDetails = @{ ExceptionType = 'System.IO.FileLoadException' ExceptionMessage = 'Could not read the koan file "{0}". The file is blocked and may have been copied from an Internet location. Use the Unblock-File to remove the block on the file.' -f $FileInfo.FullName diff --git a/PSKoans/Private/Invoke-Koan.ps1 b/PSKoans/Private/Invoke-Koan.ps1 index 61c1a3f4a..f385ebbb9 100644 --- a/PSKoans/Private/Invoke-Koan.ps1 +++ b/PSKoans/Private/Invoke-Koan.ps1 @@ -25,6 +25,11 @@ [hashtable] $ParameterSplat ) + begin { + if (-not $script:KoanRunspace) { + $script:KoanRunspace = New-KoanRunspace + } + } end { try { $Requirements = [System.Management.Automation.Language.Parser]::ParseFile( @@ -34,42 +39,47 @@ ).Ast.ScriptRequirements $Script = { - param( $Params, $RequiredModules, $PSKoansPath, $PSModulePath ) - - [System.Collections.Generic.HashSet[string]] $ModulePaths = @( - $PSModulePath -split [System.IO.Path]::PathSeparator - $env:PSModulePath -split [System.IO.Path]::PathSeparator - ) - - $env:PSModulePath = $ModulePaths -join [System.IO.Path]::PathSeparator - - Get-Module $PSKoansPath -ListAvailable | Import-Module - foreach ($module in $RequiredModules) { - Import-Module $module + param( $Params, $RequiredModules, $PSModulePath ) + + $oldModulePath = $env:PSModulePath + try { + $env:PSModulePath = @( + $PSKoansPath | Split-Path -Parent + $PSModulePath -split [System.IO.Path]::PathSeparator + $env:PSModulePath -split [System.IO.Path]::PathSeparator + ) -join [System.IO.Path]::PathSeparator + + foreach ($module in $RequiredModules) { + Import-Module $module + } + + . ([scriptblock]::Create('using module PSKoans')) + Invoke-Pester @Params + } + finally { + $env:PSModulePath = $oldModulePath } - - Invoke-Pester @Params } - $Runspace = [powershell]::Create() - $Runspace.AddScript($Script) > $null - $Runspace.AddParameter('Params', $ParameterSplat) > $null - $Runspace.AddParameter('PSKoansPath', $MyInvocation.MyCommand.Module.ModuleBase) > $null - $Runspace.AddParameter('PSModulePath', $env:PSModulePath) > $null + $ps = [powershell]::Create($script:KoanRunspace) + $ps.AddScript($Script, <# useLocalScope: #> $true) > $null + + $ps.AddParameter('Params', $ParameterSplat) > $null + $ps.AddParameter('PSModulePath', $env:PSModulePath) > $null if ($Requirements.RequiredModules) { - $Runspace.AddParameter('RequiredModules', $Requirements.RequiredModules) + $ps.AddParameter('RequiredModules', $Requirements.RequiredModules) } - $Status = $Runspace.BeginInvoke() + $Status = $ps.BeginInvoke() - do { Start-Sleep -Milliseconds 1 } until ($Status.IsCompleted) + do { Start-Sleep -Milliseconds 100 } until ($Status.IsCompleted) - $Result = $Runspace.EndInvoke($Status) + $Result = $ps.EndInvoke($Status) - if ($Runspace.HadErrors) { + if ($ps.HadErrors) { # These will be errors outside the test itself; better propagate them upwards. - foreach ($errorItem in $Runspace.Streams.Error) { + foreach ($errorItem in $ps.Streams.Error) { $PSCmdlet.WriteError($errorItem) } } @@ -77,7 +87,7 @@ $Result } finally { - $Runspace.Dispose() + $ps.Dispose() } } } diff --git a/PSKoans/Private/Measure-Koan.ps1 b/PSKoans/Private/Measure-Koan.ps1 index 8bbc24606..3fbc7169b 100644 --- a/PSKoans/Private/Measure-Koan.ps1 +++ b/PSKoans/Private/Measure-Koan.ps1 @@ -1,7 +1,4 @@ -using namespace System.Management.Automation -using namespace System.Management.Automation.Language - -function Measure-Koan { +function Measure-Koan { <# .SYNOPSIS Counts the number of koans in the provided ExternalScriptInfo objects. @@ -10,24 +7,15 @@ function Measure-Koan { files to find all of the 'It' blocks in order to count the total number of Pester tests present in the file. - When provided with a piped list of ExternalScriptInfo objects, sums the entire + When provided with a piped list of KoanInfo objects, sums the entire collection's 'It' blocks and returns a single integer sum. .PARAMETER KoanInfo - Takes an array of ExternalScriptInfo objects (as provided from Get-Command when - passed the path to an external .ps1 script file). + Takes an array of KoanInfo objects (as provided from Get-PSKoan). .EXAMPLE Get-Command .\KoanDirectory\*\*.ps1 | Measure-Koan 422 .NOTES - Measure-Koan is NOT designed to handle dynamic -TestCases values. It will handle - simple counts of directly attached -TestCases hashtables only if they do not - contain variables, pipelines, and other dynamic expressions. - - Handling dynamic scripts with pipelines and variables is beyond scope and will - not be handled; we'd essentially end up reimplementing the PS parser. If it can't - be got with .GetSafeValue() we simply aren't working with it. - Author: Joel Sallow Module: PSKoans .LINK @@ -38,53 +26,56 @@ function Measure-Koan { param( [Parameter(Position = 0, Mandatory, ValueFromPipeline)] [PSTypeName('PSKoans.KoanInfo')] - [object[]] + [psobject[]] $KoanInfo ) begin { $KoanCount = 0 + $oldModulePath = $env:PSModulePath + + $env:PSModulePath = @( + $MyInvocation.MyCommand.Module.ModuleBase + $env:PSModulePath -split [System.IO.Path]::PathSeparator + ) -join [System.IO.Path]::PathSeparator } process { - Write-Verbose "Parsing koan files from [$($KoanInfo.Name -join '], [')]" + Write-Verbose "Discovering koans in [$($KoanInfo.Name -join '], [')]" - # Find all Pester 'It' commands - $ItCommands = @(Get-KoanIt -Path $KoanInfo.Path) + $Result = & (Get-Module Pester) { + [CmdletBinding()] + param( + $Path, + $ExcludePath, + $SessionState + ) - # Find the -TestCases parameters - $TestCasesParameters = $ItCommands.Ast.CommandElements | Where-Object { - $_ -is [CommandParameterAst] -and - $_.ParameterName -eq 'TestCases' - } + $_Pester_State_Backup = $state.PSObject.Copy() + $state.Stack = [System.Collections.Stack]@() + try { + Reset-TestSuiteState - if ($TestCasesParameters) { - # Get the right CommandElements indexes for their arguments - $Indexes = $TestCasesParameters.ForEach{$ItCommands.Ast.CommandElements.IndexOf($_) + 1} - # Get value of the argument for each -TestCases - $ParameterArgument = $ItCommands.Ast.CommandElements[$Indexes] + # to avoid Describe thinking that we run in interactive mode + $invokedViaInvokePester = $true - try { - $TestCaseCount = $ParameterArgument.SafeGetValue().Count + $fileList = Find-File -Path $Path -ExcludePath $ExcludePath -Extension '.Koans.ps1' + $containers = foreach ($file in $fileList) { + New-BlockContainerObject -File (Get-Item $file) + } + + Find-Test -BlockContainer $containers -SessionState $SessionState } - catch { - # We aren't parsing complex or variable expressions, so exit here (violently) - $ParseException = [ParseException]::new( - "Unable to parse unsafe expression in Koan -TestCase syntax.", - $_.Exception - ) - $ErrorRecord = [ErrorRecord]::new( - $ParseException, - "PSKoans.KoanAstParseError", - [ErrorCategory]::ParserError, - $ParameterValues.PipelineElements.Extent.Text - ) - $PSCmdlet.ThrowTerminatingError($ErrorRecord) + finally { + $state = $_Pester_State_Backup + Remove-Variable -Name _Pester_State_Backup } - } + } -Path $KoanInfo.Path -SessionState $PSCmdlet.SessionState - $KoanCount += $TestCaseCount + $ItCommands.Count - $TestCasesParameters.Count + $KoanCount += Measure-KoanTestBlock $Result } end { Write-Verbose "Total Koans: $KoanCount" $KoanCount + + $env:PSModulePath = $oldModulePath } } diff --git a/PSKoans/Private/Measure-KoanBlockTest.ps1 b/PSKoans/Private/Measure-KoanBlockTest.ps1 new file mode 100644 index 000000000..7304cf91d --- /dev/null +++ b/PSKoans/Private/Measure-KoanBlockTest.ps1 @@ -0,0 +1,23 @@ +function Measure-KoanTestBlock { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [psobject] + $Block + ) + + $Label = if ($Block.Name) { + "Block $($Block.Name)" + } + else { + "File $($Block.BlockContainer.Item)" + } + + $Count = $Block.Tests.Count + foreach ($testBlock in $Block.Blocks) { + $Count += Measure-KoanTestBlock $testBlock + } + + $Count + Write-Information "$Label Tests: $Count" +} diff --git a/PSKoans/Private/New-KoanRunspace.ps1 b/PSKoans/Private/New-KoanRunspace.ps1 new file mode 100644 index 000000000..b39beda97 --- /dev/null +++ b/PSKoans/Private/New-KoanRunspace.ps1 @@ -0,0 +1,43 @@ +function New-KoanRunspace { + <# + .SYNOPSIS + Creates a new runspace, with PSKoans and Pester already imported, and PSModulePath already set. + + .DESCRIPTION + Creates a new runspace for reuse with Invoke-Koan. The new runspace will be pre-populated with a fresh copy of + Pester and PSKoans imported, and with a copy of the current session state's PSModulePath, which will have the + current PSKoans parent folder in it, such that `using module` statements will find the current PSKoans module + first, avoiding issues with debug environments which may find older PSKoans modules installed instead of using the + current debug version. + + .EXAMPLE + $script:KoanRunspace = New-KoanRunspace + + .NOTES + Run scripts in a new scope to avoid scope bleed wherever possible, with the `$ps.AddScript($script, $true) overload. + #> + [CmdletBinding()] + param() + + $runspace = [runspacefactory]::CreateRunspace() + $runspace.Open() + $runspace.Name = 'PSKoans.KoanRunspace' + $ps = [powershell]::Create($runspace) + + try { + $script = { + param( $PSKoansPath ) + + Get-Module $PSKoansPath -ListAvailable | Import-Module + } + + $ps.AddScript($script) > $null + $ps.AddParameter('PSKoansPath', $MyInvocation.MyCommand.Module.ModuleBase) > $null + $ps.Invoke() > $null + + $runspace + } + finally { + $ps.Dispose() + } +} diff --git a/PSKoans/Private/New-PSKoanErrorRecord.ps1 b/PSKoans/Private/New-PSKoanErrorRecord.ps1 index 0588f03f8..e2b7fd9a8 100644 --- a/PSKoans/Private/New-PSKoanErrorRecord.ps1 +++ b/PSKoans/Private/New-PSKoanErrorRecord.ps1 @@ -40,7 +40,7 @@ function New-PSKoanErrorRecord { #> [CmdletBinding(DefaultParameterSetName = 'Default')] - [OutputType([ErrorRecord])] + [OutputType([System.Management.Automation.ErrorRecord])] param( [Parameter(ParameterSetName = 'Default')] [Alias('Type')] @@ -73,8 +73,8 @@ function New-PSKoanErrorRecord { [Parameter()] [Alias('Category')] - [ErrorCategory] - $ErrorCategory = [ErrorCategory]::NotSpecified, + [System.Management.Automation.ErrorCategory] + $ErrorCategory = [System.Management.Automation.ErrorCategory]::NotSpecified, [Parameter()] [PSObject] @@ -89,7 +89,7 @@ function New-PSKoanErrorRecord { $ErrorId = "PSKoans.$($ErrorId)" } - [ErrorRecord]::new( + [System.Management.Automation.ErrorRecord]::new( $Exception, $ErrorId, $ErrorCategory, diff --git a/PSKoans/Public/Get-Karma.ps1 b/PSKoans/Public/Get-Karma.ps1 index 9d5b09a6e..cfb05e706 100644 --- a/PSKoans/Public/Get-Karma.ps1 +++ b/PSKoans/Public/Get-Karma.ps1 @@ -59,9 +59,8 @@ Write-Verbose "Koan files retrieved: $($SortedKoanList.Count)" Write-Verbose 'Counting koans...' - [int]$TotalKoans = $SortedKoanList | Measure-Koan - if ($TotalKoans -eq 0) { + if ($SortedKoanList.Count -eq 0) { if ($Topic) { $Message = @( 'Could not find any PSKoans topics matching Topic(s): {0}.' @@ -102,6 +101,7 @@ return # Skip the rest of the function } + [int]$TotalKoans = Measure-Koan $SortedKoanList $KoansPassed = 0 foreach ($KoanFile in $SortedKoanList) { @@ -111,7 +111,7 @@ $PesterTests = Invoke-Koan @{ Script = $KoanFile.Path PassThru = $true - Show = 'None' + Output = 'None' } $KoansPassed += $PesterTests.PassedCount @@ -124,27 +124,25 @@ } $Meditation = if ($PesterTests.FailedCount -gt 0) { - $NextKoanFailed = $PesterTests.TestResult | - Where-Object Result -eq 'Failed' | - Select-Object -First 1 + $NextKoanFailed = $PesterTests.Failed[0] $script:CurrentTopic = @{ Name = $KoanFile.Topic Completed = $PesterTests.PassedCount Total = $PesterTests.TotalCount - CurrentLine = ($NextKoanFailed.StackTrace -split '\r?\n')[1] -replace ':.+' + CurrentLine = ($NextKoanFailed.ErrorRecord.DisplayStackTrace -split '\r?\n')[1] -replace ':.+' } [PSCustomObject]@{ PSTypeName = 'PSKoans.Result' - Describe = $NextKoanFailed.Describe + Describe = $NextKoanFailed.Block.Name It = $NextKoanFailed.Name - Expectation = $NextKoanFailed.ErrorRecord - Meditation = $NextKoanFailed.StackTrace + Expectation = $NextKoanFailed.ErrorRecord.DisplayErrorMessage + Meditation = $NextKoanFailed.ErrorRecord.DisplayStackTrace KoansPassed = $KoansPassed TotalKoans = $TotalKoans CurrentTopic = [PSCustomObject]$script:CurrentTopic - Results = $PesterTests.TestResult + Results = $PesterTests.Tests RequestedTopic = $Topic } } diff --git a/README.md b/README.md index db9b5bdd9..7ef670b1f 100644 --- a/README.md +++ b/README.md @@ -57,17 +57,12 @@ To install the latest version of Pester, use the appropriate command for your ve ```PowerShell # PS 5.1 (upgrade to latest Pester) -Install-Module Pester -Force -SkipPublisherCheck -Scope CurrentUser -MaximumVersion 4.99.99 +Install-Module Pester -Force -SkipPublisherCheck -Scope CurrentUser -MinimumVersion 5.0.2 # PS 6.0+ (Install Pester under current user) -Install-Module Pester -Scope CurrentUser -MaximumVersion 4.99.99 +Install-Module Pester -Scope CurrentUser -MinimumVersion 5.0.2 ``` -> :warning: **WARNING** -> -> PSKoans is not yet compatible with v5 of Pester, and I have (unwisely) neglected to set the -MaximumVersion in the PSKoans module manifest as yet. -> I will make some fixes shortly, but in the meantime you cannot have v5 of Pester installed if you would like to use PSKoans. - ## Getting Started ### Install from Gallery diff --git a/Tests/Functions/Private/Assert-UnblockedFile.Tests.ps1 b/Tests/Functions/Private/Assert-UnblockedFile.Tests.ps1 index 536c47ee6..2ab1b9ea7 100644 --- a/Tests/Functions/Private/Assert-UnblockedFile.Tests.ps1 +++ b/Tests/Functions/Private/Assert-UnblockedFile.Tests.ps1 @@ -1,59 +1,64 @@ -#region Header -if (-not (Get-Module PSKoans)) { - $moduleBase = Join-Path -Path $psscriptroot.Substring(0, $psscriptroot.IndexOf('\Tests')) -ChildPath 'PSKoans' +#Requires -Modules PSKoans - Import-Module $moduleBase -Force -} -#endregion - -if ($PSVersionTable.PSEdition -eq 'Desktop' -or $PSVersionTable.Platform -eq 'Win32NT') { - InModuleScope PSKoans { - Describe Assert-UnblockedFile { - BeforeAll { - $defaultParams = @{ - FileInfo = [System.IO.FileInfo](Join-Path -Path $TestDrive -ChildPath 'AboutSomething.Koans.ps1') - PassThru = $true - } - } +#region Discovery +$SkipTests = $PSVersionTable.PSEdition -ne 'Desktop' -or $PSVersionTable.Platform -ne 'Win32NT' +#endregion Discovery - BeforeEach { - Set-Content -Path $defaultParams.FileInfo.FullName -Value @' - using module PSKoans +Describe 'Assert-UnblockedFile' -Skip:$SkipTests { - [Koan(Position = 1)] - param ( ) + BeforeAll { + $defaultParams = @{ + FileInfo = [System.IO.FileInfo](Join-Path -Path $TestDrive -ChildPath 'AboutSomething.Koans.ps1') + PassThru = $true + } + } - Describe 'About something' { - It 'Has examples' { - $true | Should -BeTrue - } - } -'@ - } + BeforeEach { + Set-Content -Path $defaultParams.FileInfo.FullName -Value @' + using module PSKoans + [Koan(Position = 1)] + param() + + Describe 'About something' { - AfterEach { - Remove-Item -Path $defaultParams.FileInfo.FullName + It 'Has examples' { + $true | Should -BeTrue + } } +'@ + } - Context 'File is blocked' { - BeforeEach { - Set-Content -Path $defaultParams.FileInfo.FullName -Stream Zone.Identifier -Value @' - [ZoneTransfer] - ZoneId=3 - ReferrerUrl=C:\Downloads\File.zip + AfterEach { + Remove-Item -Path $defaultParams.FileInfo.FullName + } + + Context 'File With External Zone Identifier' { + + BeforeEach { + Set-Content -Path $defaultParams.FileInfo.FullName -Stream Zone.Identifier -Value @' + [ZoneTransfer] + ZoneId=3 + ReferrerUrl=C:\Downloads\File.zip '@ - } + } - It 'should throw a terminating error if the file is blocked' { - { Assert-UnblockedFile @defaultParams } | Should -Throw -ErrorId 'PSKoans.KoanFileIsBlocked' + It 'should throw a terminating error if the file is blocked' { + { + InModuleScope 'PSKoans' -Parameters @{ Params = $defaultParams } { + param($Params) + Assert-UnblockedFile @Params } - } + } | Should -Throw -ErrorId 'PSKoans.KoanFileIsBlocked' + } + } - Context 'File is not blocked' { - It 'returns the original object with -PassThru if the file is not blocked' { - Assert-UnblockedFile @defaultParams | Should -BeOfType [System.IO.FileInfo] - } - } + Context 'File Without Zone Identifier' { + + It 'returns the original object with -PassThru if the file is not blocked' { + InModuleScope 'PSKoans' -Parameters @{ Params = $defaultParams } { + param($Params) + Assert-UnblockedFile @Params + } | Should -BeOfType [System.IO.FileInfo] } } } diff --git a/Tests/Functions/Private/ConvertFrom-WildcardPattern.Tests.ps1 b/Tests/Functions/Private/ConvertFrom-WildcardPattern.Tests.ps1 index 83cf5d1c8..96ac71f97 100644 --- a/Tests/Functions/Private/ConvertFrom-WildcardPattern.Tests.ps1 +++ b/Tests/Functions/Private/ConvertFrom-WildcardPattern.Tests.ps1 @@ -1,29 +1,26 @@ #Requires -Modules PSKoans -InModuleScope 'PSKoans' { - Describe 'ConvertFrom-WildcardPattern' { +Describe 'ConvertFrom-WildcardPattern' { - It 'adds start and end of string anchors to an explicit value' { - ConvertFrom-WildcardPattern 'AboutArrays' | Should -Be '^AboutArrays$' - } - - It 'joins multiple expressions with or' { - ConvertFrom-WildcardPattern 'AboutArrays', 'AboutComparison' | Should -Be '^AboutArrays$|^AboutComparison$' - } + It 'adds start and end of string anchors to an explicit value' { + InModuleScope 'PSKoans' { ConvertFrom-WildcardPattern 'AboutArrays' } | Should -Be '^AboutArrays$' + } + It 'joins multiple expressions with |' { + InModuleScope 'PSKoans' { ConvertFrom-WildcardPattern 'AboutArrays', 'AboutComparison' } | + Should -Be '^AboutArrays$|^AboutComparison$' + } - It 'Replaces wildcard characters with regex equivalent' -TestCases @( - @{ Pattern = 'About*son'; Expected = '^About.*son$' } - @{ Pattern = 'AboutArrays*'; Expected = '^AboutArrays' } - @{ Pattern = '*Arrays'; Expected = 'Arrays$' } - @{ Pattern = '*Array*'; Expected = 'Array' } - ) { - param ( - $Pattern, - $Expected - ) + It 'replaces wildcard characters with regex equivalent' -TestCases @( + @{ Pattern = 'About*son'; Expected = '^About.*son$' } + @{ Pattern = 'AboutArrays*'; Expected = '^AboutArrays' } + @{ Pattern = '*Arrays'; Expected = 'Arrays$' } + @{ Pattern = '*Array*'; Expected = 'Array' } + ) { + InModuleScope 'PSKoans' -Parameters @{ Pattern = $Pattern } { + param($Pattern) - ConvertFrom-WildcardPattern $Pattern | Should -Be $Expected - } + ConvertFrom-WildcardPattern $Pattern + } | Should -Be $Expected } } diff --git a/Tests/Functions/Private/Get-KoanAst.Tests.ps1 b/Tests/Functions/Private/Get-KoanAst.Tests.ps1 index 04551fcb6..cb16f9339 100644 --- a/Tests/Functions/Private/Get-KoanAst.Tests.ps1 +++ b/Tests/Functions/Private/Get-KoanAst.Tests.ps1 @@ -1,57 +1,66 @@ -#region Header -if (-not (Get-Module PSKoans)) { - $moduleBase = Join-Path -Path $psscriptroot.Substring(0, $psscriptroot.IndexOf('\Tests')) -ChildPath 'PSKoans' +#Requires -Modules PSKoans - Import-Module $moduleBase -Force -} -#endregion - -InModuleScope PSKoans { - Describe Get-KoanAst { - BeforeAll { - $path = Join-Path $TestDrive 'AboutSomething.Koans.ps1' - - Set-Content -Path $path -Value @' - using module PSKoans - [Koan(Position = 1)] - param() - <# - About Something - #> - Describe 'Something' { - It 'Has some examples' { - $true | Should -BeTrue - } +Describe 'Get-KoanAst' { + + BeforeAll { + $path = Join-Path $TestDrive 'AboutSomething.Koans.ps1' + + Set-Content -Path $path -Value @' + using module PSKoans + [Koan(Position = 1)] + param() + <# + About Something + #> + Describe 'Something' { + It 'Has some examples' { + $true | Should -BeTrue } + } '@ + } + + It 'excludes the "using module PSKoans" statement from the AST' { + $ast = InModuleScope 'PSKoans' -Parameters @{ Path = $path } { + param($Path) + Get-KoanAst -Path $Path } - It 'Excludes the "using module PSKoans" statement from the AST' { - $ast = Get-KoanAst -Path $path + $ast.UsingStatements | Should -BeNullOrEmpty + } - $ast.UsingStatements | Should -BeNullOrEmpty - } + It 'maintains consistency of position data when reading and modifying the source' { + $tokens = $errors = $null + $originalAst = [System.Management.Automation.Language.Parser]::ParseFile( + $path, + [Ref]$tokens, + [Ref]$errors + ) - It 'When reading and modifying the source, position data is consistent' { - $tokens = $errors = @() - $originalAst = [System.Management.Automation.Language.Parser]::ParseFile( - $path, - [Ref]$tokens, - [Ref]$errors - ) - $originalItBlock = $originalAst.Find( { + $originalItBlock = $originalAst.Find( + { $args[0] -is [System.Management.Automation.Language.CommandAst] -and $args[0].GetCommandName() -eq 'It' - }, $true) + }, + $true + ) - $modifiedAst = Get-KoanAst -Path $path - $modifiedItBlock = $modifiedAst.Find( { + $modifiedAst = InModuleScope 'PSKoans' -Parameters @{ Path = $path } { + param($Path) + Get-KoanAst -Path $Path + } + + $modifiedItBlock = $modifiedAst.Find( + { $args[0] -is [System.Management.Automation.Language.CommandAst] -and $args[0].GetCommandName() -eq 'It' - }, $true) + }, + $true + ) - $modifiedItBlock.Extent.StartOffset | Should -Be $originalItBlock.Extent.StartOffset - $modifiedItBlock.Extent.EndOffset | Should -Be $originalItBlock.Extent.EndOffset - } + $modifiedItBlock.Extent.StartOffset | + Should -Be $originalItBlock.Extent.StartOffset -Because 'the start offsets should match' + $modifiedItBlock.Extent.EndOffset | + Should -Be $originalItBlock.Extent.EndOffset -Because 'the end offsets should match' } } diff --git a/Tests/Functions/Private/Get-KoanAttribute.Tests.ps1 b/Tests/Functions/Private/Get-KoanAttribute.Tests.ps1 index 72fe2d14a..2ba7d11ff 100644 --- a/Tests/Functions/Private/Get-KoanAttribute.Tests.ps1 +++ b/Tests/Functions/Private/Get-KoanAttribute.Tests.ps1 @@ -1,141 +1,159 @@ -#region Header -if (-not (Get-Module PSKoans)) { - $moduleBase = Join-Path -Path $psscriptroot.Substring(0, $psscriptroot.IndexOf('\Tests')) -ChildPath 'PSKoans' +#Requires -Modules PSKoans - Import-Module $moduleBase -Force -} -#endregion +Describe 'Get-KoanAttribute' { -InModuleScope PSKoans { - Describe Get-KoanAttribute { - BeforeAll { - $defaultParams = @{ - Path = Join-Path $TestDrive 'AboutSomething.Koans.ps1' - } + BeforeAll { + $filePath = @{ + Path = Join-Path $TestDrive -ChildPath 'AboutSomething.Koans.ps1' } + } - Context 'Content has no errors' { - BeforeAll { - Mock Get-KoanAst { - { - [Koan(Position = 1)] - param( ) - - Describe 'About something' { - It 'Has examples' { - $true | Should -BeTrue - } - } - }.Ast - } - } - - It 'Gets the position argument from the Koan attribute' { - $attributeInfo = Get-KoanAttribute @defaultParams + Context 'Content has no errors' { - $attributeInfo | Should -Not -BeNullOrEmpty - $attributeInfo.Position | Should -Be 1 - } + BeforeAll { + Mock 'Get-KoanAst' -ModuleName 'PSKoans' { + { + [Koan(Position = 1)] + param() - It 'When Module is not set, uses a default value for Module' { - $attributeInfo = Get-KoanAttribute @defaultParams + Describe 'About something' { - $attributeInfo | Should -Not -BeNullOrEmpty - $attributeInfo.Module | Should -Be ([KoanAttribute]::new().Module) + It 'Has examples' { + $true | Should -BeTrue + } + } + }.Ast } } - Context 'Module declared' { - BeforeAll { - Mock Get-KoanAst { - { - [Koan(Position = 1, Module = 'Name')] - param( ) - - Describe 'About something' { - It 'Has examples' { - $true | Should -BeTrue - } - } - }.Ast - } + It 'gets the position argument from the Koan attribute' { + $attributeInfo = InModuleScope 'PSKoans' -Parameters $filePath { + param($Path) + Get-KoanAttribute $Path } - It 'When Module is set, retrieves the value for the module' { - $attributeInfo = Get-KoanAttribute @defaultParams + $attributeInfo | Should -Not -BeNullOrEmpty + $attributeInfo.Position | Should -Be 1 + } - $attributeInfo | Should -Not -BeNullOrEmpty - $attributeInfo.Position | Should -Be 1 - $attributeInfo.Module | Should -Be 'Name' + It 'uses a default value for Module when it is not set' { + $attributeInfo = InModuleScope 'PSKoans' -Parameters $filePath { + param($Path) + Get-KoanAttribute $Path } + + $attributeInfo | Should -Not -BeNullOrEmpty + $attributeInfo.Module | Should -Be ([KoanAttribute]::new().Module) } + } + + Context 'Module declared' { + + BeforeAll { + Mock 'Get-KoanAst' -ModuleName 'PSKoans' { + { + [Koan(Position = 1, Module = 'Name')] + param() - Context 'Full attribute name used' { - BeforeAll { - Mock Get-KoanAst { - { - [KoanAttribute(Position = 1)] - param( ) - - Describe 'About something' { - It 'Has examples' { - $true | Should -BeTrue - } + Describe 'About something' { + + It 'has examples' { + $true | Should -BeTrue } - }.Ast - } + } + }.Ast } + } - It 'Returns attribute information the full name is used' { - $attributeInfo = Get-KoanAttribute @defaultParams - - $attributeInfo | Should -Not -BeNullOrEmpty - $attributeInfo.Position | Should -Be 1 + It 'retrieves the value for the module when it is set' { + $attributeInfo = InModuleScope 'PSKoans' -Parameters $filePath { + param($Path) + Get-KoanAttribute $Path } + + $attributeInfo | Should -Not -BeNullOrEmpty + $attributeInfo.Position | Should -Be 1 + $attributeInfo.Module | Should -Be 'Name' } + } - Context 'Content has errors' { - BeforeAll { - Set-Content @defaultParams -Value @' - [Koan(Position = 1)] + Context 'Full attribute name used' { + + BeforeAll { + Mock 'Get-KoanAst' -ModuleName 'PSKoans' { + { + [KoanAttribute(Position = 1)] param( ) Describe 'About something' { It 'Has examples' { - -not ____ | Should -BeTrue + $true | Should -BeTrue } } -'@ + }.Ast } + } - It 'Retrieves the Koan attribute if the file has syntax errors' { - $attributeInfo = Get-KoanAttribute @defaultParams - - $attributeInfo | Should -Not -BeNullOrEmpty - $attributeInfo.Position | Should -Be 1 + It 'still returns attribute information when the full name is used' { + $attributeInfo = InModuleScope 'PSKoans' -Parameters $filePath { + param($Path) + Get-KoanAttribute $Path } + + $attributeInfo | Should -Not -BeNullOrEmpty + $attributeInfo.Position | Should -Be 1 } + } - Context 'Attribute is missing' { - BeforeAll { - Mock Get-KoanAst { - { - param( ) + Context 'Content has errors' { - Describe 'About something' { - It 'Has examples' { - $true | Should -BeTrue - } - } - }.Ast + BeforeAll { + Set-Content @filePath -Value @' + [Koan(Position = 1)] + param() + + Describe 'About something' { + It 'has examples' { + -not ____ | Should -BeTrue + } } +'@ + } + + It 'retrieves the Koan attribute even if the file has syntax errors' { + $attributeInfo = InModuleScope 'PSKoans' -Parameters $filePath { + param($Path) + Get-KoanAttribute $Path } - It 'When the Koan attribute is missing, returns nothing' { - $attributeInfo = Get-KoanAttribute @defaultParams + $attributeInfo | Should -Not -BeNullOrEmpty + $attributeInfo.Position | Should -Be 1 + } + } + + Context 'Attribute is missing' { + + BeforeAll { + Mock 'Get-KoanAst' -ModuleName 'PSKoans' { + { + param() + + Describe 'About something' { + It 'Has examples' { + $true | Should -BeTrue + } + } + }.Ast + } + } - $attributeInfo | Should -BeNullOrEmpty + It 'When the Koan attribute is missing, returns nothing' { + $attributeInfo = InModuleScope 'PSKoans' -Parameters $filePath { + param($Path) + Get-KoanAttribute $Path } + + $attributeInfo | Should -BeNullOrEmpty } } } diff --git a/Tests/Functions/Private/Get-KoanIt.Tests.ps1 b/Tests/Functions/Private/Get-KoanIt.Tests.ps1 index a04085571..6f902cb98 100644 --- a/Tests/Functions/Private/Get-KoanIt.Tests.ps1 +++ b/Tests/Functions/Private/Get-KoanIt.Tests.ps1 @@ -1,50 +1,49 @@ -#region Header -if (-not (Get-Module PSKoans)) { - $moduleBase = Join-Path -Path $psscriptroot.Substring(0, $psscriptroot.IndexOf('\Tests')) -ChildPath 'PSKoans' +#Requires -Modules PSKoans - Import-Module $moduleBase -Force -} -#endregion +Describe 'Get-KoanIt' { -InModuleScope PSKoans { - Describe Get-KoanIt { - BeforeAll { - $defaultParams = @{ - Path = Join-Path $TestDrive 'AboutSomething.Koans.ps1' - } + BeforeAll { + $defaultParams = @{ + Path = Join-Path $TestDrive 'AboutSomething.Koans.ps1' } + } - Context 'Content has no errors' { - BeforeAll { - Mock Get-KoanAst { - { - [Koan(Position = 1)] - param( ) + Context 'Content has no errors' { - Describe 'About something' { - It 'first' { - $true | Should -BeTrue - } + BeforeAll { + Mock 'Get-KoanAst' -ModuleName 'PSKoans' { + { + [Koan(Position = 1)] + param( ) - It 'second' { - $true | Should -BeTrue - } + Describe 'About something' { + It 'first' { + $true | Should -BeTrue } - }.Ast - } - } - It 'returns all information about all It blocks' { - $ItCommands = Get-KoanIt @defaultParams + It 'second' { + $true | Should -BeTrue + } + } + }.Ast + } + } - $ItCommands | Should -Not -BeNullOrEmpty - $ItCommands.Count | Should -Be 2 + It 'returns all information about all It blocks' { + $ItCommands = InModuleScope 'PSKoans' -Parameters $defaultParams { + param($Path) + Get-KoanIt $Path } + + $ItCommands | Should -Not -BeNullOrEmpty + $ItCommands.Count | Should -Be 2 } + } - Context 'Content has errors' { - BeforeAll { - Set-Content @defaultParams -Value @' + Context 'Content has errors' { + + BeforeAll { + Set-Content @defaultParams -Value @' [Koan(Position = 1)] param( ) @@ -58,14 +57,16 @@ InModuleScope PSKoans { } } '@ - } - - It 'Retrieves all It blocks when the file has syntax errors' { - $ItCommands = Get-KoanIt @defaultParams + } - $ItCommands | Should -Not -BeNullOrEmpty - $ItCommands.Count | Should -Be 2 + It 'Retrieves all It blocks when the file has syntax errors' { + $ItCommands = InModuleScope 'PSKoans' -Parameters $defaultParams { + param($Path) + Get-KoanIt $Path } + + $ItCommands | Should -Not -BeNullOrEmpty + $ItCommands.Count | Should -Be 2 } } } diff --git a/Tests/Functions/Private/Invoke-Koan.Tests.ps1 b/Tests/Functions/Private/Invoke-Koan.Tests.ps1 index d8cfeefc6..10e58523d 100644 --- a/Tests/Functions/Private/Invoke-Koan.Tests.ps1 +++ b/Tests/Functions/Private/Invoke-Koan.Tests.ps1 @@ -1,34 +1,46 @@ #Requires -Modules PSKoans -${global:Test File} = "$PSScriptRoot/ControlTests/Invoke-Koan.Control_Tests.ps1" +Describe 'Invoke-Koan' { -InModuleScope 'PSKoans' { - Describe 'Invoke-Koan' { - - It 'will not error out' { - { - Invoke-Koan @{ Script = ${global:Test File} } - } | Should -Not -Throw - } + BeforeAll { + $testFile = @{ Script = "$PSScriptRoot/ControlTests/Invoke-Koan.Control_Tests.ps1" } + } - It 'will produce output with -Passthru' { - Invoke-Koan @{ Script = ${global:Test File}; PassThru = $true } | Should -Not -BeNullOrEmpty - } + It 'runs the test successfully' { + { + InModuleScope 'PSKoans' -Parameters $testFile { + param($Script) + Invoke-Koan @{ Script = $Script } + } + } | Should -Not -Throw + } - It 'will correctly report test results' { - $Results = Invoke-Koan @{ Script = ${global:Test File}; PassThru = $true } + It 'produces output with -Passthru' { + InModuleScope 'PSKoans' -Parameters $testFile { + param($Script) + Invoke-Koan @{ Script = $Script; PassThru = $true } + } | Should -Not -BeNullOrEmpty + } - $Results.TotalCount | Should -Be 2 - $Results.PassedCount | Should -Be 0 - $Results.FailedCount | Should -Be 2 + It 'correctly reports test results' { + $Results = InModuleScope 'PSKoans' -Parameters $testFile { + param($Script) + Invoke-Koan @{ Script = $Script; PassThru = $true } } - It 'reports only expected exception types' { - $Results = Invoke-Koan @{ Script = ${global:Test File}; PassThru = $true } + $Results.TotalCount | Should -Be 2 + $Results.PassedCount | Should -Be 0 + $Results.FailedCount | Should -Be 2 + } - $Results.TestResult.ErrorRecord.Exception | - ForEach-Object -MemberName GetType | - Should -Be @([Exception], [NotImplementedException]) + It 'reports only expected exception types' { + $Results = InModuleScope 'PSKoans' -Parameters $testFile { + param($Script) + Invoke-Koan @{ Script = $Script; PassThru = $true } } + + $Results.Tests.ErrorRecord.Exception | + ForEach-Object -MemberName GetType | + Should -Be @([Exception], [NotImplementedException]) } } diff --git a/Tests/Functions/Private/Measure-Koan.Tests.ps1 b/Tests/Functions/Private/Measure-Koan.Tests.ps1 index 7f9a0ffa5..577a82d60 100644 --- a/Tests/Functions/Private/Measure-Koan.Tests.ps1 +++ b/Tests/Functions/Private/Measure-Koan.Tests.ps1 @@ -1,96 +1,90 @@ -#region Header -if (-not (Get-Module PSKoans)) { - $moduleBase = Join-Path -Path $psscriptroot.Substring(0, $psscriptroot.IndexOf('\Tests')) -ChildPath 'PSKoans' +#Requires -Modules PSKoans - Import-Module $moduleBase -Force -} -#endregion +Describe 'Measure-Koan' -Skip { -InModuleScope 'PSKoans' { - Describe 'Measure-Koan' { - BeforeAll { - Set-Content -Path TestDrive:\TestCases.Koans.ps1 -Value @' - Describe 'Test cases param' { - It 'first ' -TestCases @( - @{ TestCase = 1 } - @{ TestCase = 2 } - @{ TestCase = 3 } - ) { - param($TestCase) + BeforeAll { + Set-Content -Path 'TestDrive:\TestCases.Koans.ps1' -Value @' + Describe 'Test cases param' { + It 'first ' -TestCases @( + @{ TestCase = 1 } + @{ TestCase = 2 } + @{ TestCase = 3 } + ) { + param($TestCase) - $TestCase | Should -BeOfType int - } + $TestCase | Should -BeOfType int } + } '@ - Set-Content -Path TestDrive:\JustIt.Koans.ps1 -Value @' - Describe 'Just it' { - It 'first' { - $true | Should -BeTrue - } + Set-Content -Path 'TestDrive:\JustIt.Koans.ps1' -Value @' + Describe 'Just it' { + It 'first' { + $true | Should -BeTrue + } - It 'second' { - $true | Should -BeTrue - } - } + It 'second' { + $true | Should -BeTrue + } + } '@ - Set-Content -Path TestDrive:\Mixed.Koans.ps1 -Value @' - Describe 'Mixed' { - It 'first' { - $true | Should -BeTrue - } + Set-Content -Path TestDrive:\Mixed.Koans.ps1 -Value @' + Describe 'Mixed' { + It 'first' { + $true | Should -BeTrue + } - It 'second ' -TestCases @( - @{ TestCase = 1 } - @{ TestCase = 2 } - ) { - param($TestCase) + It 'second ' -TestCases @( + @{ TestCase = 1 } + @{ TestCase = 2 } + ) { + param($TestCase) - $TestCase | Should -BeOfType int - } - } + $TestCase | Should -BeOfType int + } + } '@ - Set-Content -Path TestDrive:\MutlipleTestCases.Koans.ps1 -Value @' - Describe 'Test cases param' { - It 'first ' -TestCases @( - @{ TestCase = 1 } - @{ TestCase = 2 } - @{ TestCase = 3 } - ) { - param($TestCase) - - $TestCase | Should -BeOfType int - } + Set-Content -Path TestDrive:\MutlipleTestCases.Koans.ps1 -Value @' + Describe 'Test cases param' { + It 'first ' -TestCases @( + @{ TestCase = 1 } + @{ TestCase = 2 } + @{ TestCase = 3 } + ) { + param($TestCase) - It 'second ' -TestCases @( - @{ TestCase = 1 } - @{ TestCase = 2 } - @{ TestCase = 3 } - ) { - param($TestCase) - - $TestCase | Should -BeOfType int - } - } -'@ - } + $TestCase | Should -BeOfType int + } - It 'counts the number of tests in ' -TestCases @( - @{ Path = 'TestDrive:\TestCases.Koans.ps1'; ExpectedValue = 3 } - @{ Path = 'TestDrive:\JustIt.Koans.ps1'; ExpectedValue = 2 } - @{ Path = 'TestDrive:\Mixed.Koans.ps1'; ExpectedValue = 3 } - @{ Path = 'TestDrive:\MutlipleTestCases.Koans.ps1'; ExpectedValue = 6 } - ) { - param($Path, $ExpectedValue) + It 'second ' -TestCases @( + @{ TestCase = 1 } + @{ TestCase = 2 } + @{ TestCase = 3 } + ) { + param($TestCase) - $koanInfo = [PSCustomObject]@{ - Path = $Path - PSTypeName = 'PSKoans.KoanInfo' + $TestCase | Should -BeOfType int + } } +'@ + } - $koanInfo | Measure-Koan | Should -Be $ExpectedValue + It 'correctly counts the number of tests in , including -TestCases' -TestCases @( + @{ Path = 'TestDrive:\TestCases.Koans.ps1'; ExpectedValue = 3 } + @{ Path = 'TestDrive:\JustIt.Koans.ps1'; ExpectedValue = 2 } + @{ Path = 'TestDrive:\Mixed.Koans.ps1'; ExpectedValue = 3 } + @{ Path = 'TestDrive:\MutlipleTestCases.Koans.ps1'; ExpectedValue = 6 } + ) { + $koanInfo = [PSCustomObject]@{ + Path = $Path + PSTypeName = 'PSKoans.KoanInfo' } + + InModuleScope 'PSKoans' -Parameters @{ Koan = $koanInfo } { + param($Koan) + Measure-Koan $Koan + } | Should -Be $ExpectedValue } } diff --git a/Tests/Functions/Private/New-KoanRunspace.Tests.ps1 b/Tests/Functions/Private/New-KoanRunspace.Tests.ps1 new file mode 100644 index 000000000..6155de163 --- /dev/null +++ b/Tests/Functions/Private/New-KoanRunspace.Tests.ps1 @@ -0,0 +1,34 @@ +Describe 'New-KoanRunspace' { + + BeforeAll { + [runspace]$runspace = $null + } + + AfterEach { + $runspace.Dispose() + } + + It 'creates a new runspace' { + $runspace = InModuleScope 'PSKoans' { New-KoanRunspace } + $runspace | Should -BeOfType [runspace] + } + + It 'names the new runspace' { + $runspace = InModuleScope 'PSKoans' { New-KoanRunspace } + $runspace.Name | Should -Be 'PSKoans.KoanRunspace' + } + + It 'preloads the runspace with PSKoans' { + $runspace = InModuleScope 'PSKoans' { New-KoanRunspace } + $ps = [powershell]::Create($runspace) + + try { + $ps.AddCommand('Get-Module').AddParameter('Name', 'PSKoans') > $null + $module = $ps.Invoke() + $module.Name | Should -eq 'PSKoans' + } + finally { + $ps.Dispose() + } + } +} diff --git a/Tests/Functions/Private/Update-PSKoanFile.Tests.ps1 b/Tests/Functions/Private/Update-PSKoanFile.Tests.ps1 index a295b4b95..357df55bd 100644 --- a/Tests/Functions/Private/Update-PSKoanFile.Tests.ps1 +++ b/Tests/Functions/Private/Update-PSKoanFile.Tests.ps1 @@ -1,86 +1,81 @@ -#region Header -if (-not (Get-Module PSKoans)) { - $moduleBase = Join-Path -Path $psscriptroot.Substring(0, $psscriptroot.IndexOf('\Tests')) -ChildPath 'PSKoans' +#Requires -Modules PSKoans - Import-Module $moduleBase -Force -} -#endregion +Describe 'Update-PSKoanFile' { -InModuleScope 'PSKoans' { - Describe 'Update-PSKoanFile' { - BeforeAll { - Mock Get-PSKoanLocation { - Join-Path -Path $TestDrive -ChildPath 'Koans' - } - Mock Get-PSKoan { - [PSCustomObject]@{ - Topic = 'AboutSomething' - Path = Join-Path -Path $TestDrive -ChildPath 'Module/Group/AboutSomething.ps1' - RelativePath = 'Group/AboutSomething.ps1' - } + BeforeAll { + Mock 'Get-PSKoanLocation' { + Join-Path -Path $TestDrive -ChildPath 'Koans' + } + + Mock 'Get-PSKoan' { + [PSCustomObject]@{ + Topic = 'AboutSomething' + Path = Join-Path -Path $TestDrive -ChildPath 'Module/Group/AboutSomething.ps1' + RelativePath = 'Group/AboutSomething.ps1' } - New-Item -Path (Join-Path -Path $TestDrive -ChildPath 'Koans/Group') -ItemType Directory - New-Item -Path (Join-Path -Path $TestDrive -ChildPath 'Module/Group') -ItemType Directory + } - Set-Content -Path (Get-PSKoan).Path -Value @' - Describe 'AboutSomething' { - It 'koan 1' { - __ | Should -Be 1 - } + New-Item -Path (Join-Path -Path $TestDrive -ChildPath 'Koans/Group') -ItemType Directory + New-Item -Path (Join-Path -Path $TestDrive -ChildPath 'Module/Group') -ItemType Directory - It 'koan 2' { - __ | Should -Be 2 - } + Set-Content -Path (Get-PSKoan).Path -Value @' + Describe 'AboutSomething' { + It 'koan 1' { + __ | Should -Be 1 + } + + It 'koan 2' { + __ | Should -Be 2 + } - Context 'first' { - It 'koan 3' { - __ | Should -Be 3 - } + Context 'first' { + It 'koan 3' { + __ | Should -Be 3 } + } - Context 'second' { - It 'koan 4' { - __ | Should -Be 4 - } + Context 'second' { + It 'koan 4' { + __ | Should -Be 4 } } + } '@ - $userFilePath = Join-Path -Path (Get-PSKoanLocation) -ChildPath (Get-PSKoan).RelativePath - } + $userFilePath = Join-Path -Path (Get-PSKoanLocation) -ChildPath (Get-PSKoan).RelativePath + } - BeforeEach { - Set-Content -Path $userFilePath -Value @' - Describe 'AboutSomething' { - It 'koan 1' { - 1 | Should -Be 1 - } + BeforeEach { + Set-Content -Path $userFilePath -Value @' + Describe 'AboutSomething' { + It 'koan 1' { + 1 | Should -Be 1 + } - It 'koan 2' { - __ | Should -Be 2 - } + It 'koan 2' { + __ | Should -Be 2 + } - Context 'second' { - It 'koan 4' { - 4 | Should -Be 4 - } + Context 'second' { + It 'koan 4' { + 4 | Should -Be 4 } } + } '@ - } + } - It 'should replay completed koans' { - Update-PSKoanFile -Topic AboutSomething -Confirm:$false + It 'should replay completed koans' { + InModuleScope 'PSKoans' { Update-PSKoanFile -Topic AboutSomething -Confirm:$false } - $userFilePath | Should -FileContentMatch '1 | Should -Be 1' - $userFilePath | Should -FileContentMatch '__ | Should -Be 2' - $userFilePath | Should -FileContentMatch '4 | Should -Be 4' - } + $userFilePath | Should -FileContentMatch '1 | Should -Be 1' + $userFilePath | Should -FileContentMatch '__ | Should -Be 2' + $userFilePath | Should -FileContentMatch '4 | Should -Be 4' + } - It 'should should allow new koans to be inserted' { - Update-PSKoanFile -Topic AboutSomething -Confirm:$false + It 'should should allow new koans to be inserted' { + InModuleScope 'PSKoans' { Update-PSKoanFile -Topic AboutSomething -Confirm:$false } - $userFilePath | Should -FileContentMatch 'koan 3' - } + $userFilePath | Should -FileContentMatch 'koan 3' } } diff --git a/Tests/Functions/Public/Get-Blank.Tests.ps1 b/Tests/Functions/Public/Get-Blank.Tests.ps1 index 34bd04af8..f24e30556 100644 --- a/Tests/Functions/Public/Get-Blank.Tests.ps1 +++ b/Tests/Functions/Public/Get-Blank.Tests.ps1 @@ -1,14 +1,15 @@ #Requires -Modules PSKoans Describe 'Get-Blank' { + It 'should not produce output' { Get-Blank | Should -BeNullOrEmpty } - + It 'should be usable in the middle of a pipeline' { { Get-Variable | Get-Blank | Write-Output } | Should -Not -Throw } - + It 'should quietly ignore parameters' { { Get-Blank -Param1 Value1 -Param2 Value2 } | Should -Not -Throw } diff --git a/Tests/Functions/Public/Get-Karma.Tests.ps1 b/Tests/Functions/Public/Get-Karma.Tests.ps1 index 14b15d20a..7a25cce12 100644 --- a/Tests/Functions/Public/Get-Karma.Tests.ps1 +++ b/Tests/Functions/Public/Get-Karma.Tests.ps1 @@ -1,156 +1,165 @@ -#region Header -if (-not (Get-Module PSKoans)) { - $moduleBase = Join-Path -Path $psscriptroot.Substring(0, $psscriptroot.IndexOf('\Tests')) -ChildPath 'PSKoans' +#Requires -Modules PSKoans - Import-Module $moduleBase -Force -} -#endregion +Describe 'Get-Karma' { -InModuleScope 'PSKoans' { - Describe 'Get-Karma' { - BeforeAll { - $TestLocation = Join-Path -Path $TestDrive -ChildPath 'PSKoans' + BeforeAll { + $originalLocation = Get-PSKoanLocation + Set-PSKoanLocation -Path (Join-Path $TestDrive -ChildPath 'PSKoans') + Update-PSKoan -Confirm:$false + } - Mock Get-PSKoanLocation { - $TestLocation - } + AfterAll { + Set-PSKoanLocation -Path $originalLocation + } - Update-PSKoan -Confirm:$false - } + Context 'Default Behaviour' { - Context 'Default Behaviour' { - BeforeAll { - Mock Invoke-Koan -ModuleName 'PSKoans' { - [PSCustomObject]@{ - PassedCount = 0 - FailedCount = 4 - } + BeforeAll { + Mock 'Measure-Koan' -MockWith { 4 } -ModuleName 'PSKoans' + Mock 'Invoke-Koan' -ModuleName 'PSKoans' { + [PSCustomObject]@{ + PassedCount = 0 + FailedCount = 4 + Failed = @() } } - It 'should produce a hashtable with data' { - $Result = Get-Karma - $Result.PSTypeNames[0] | Should -Be 'PSKoans.Result' - $Result.KoansPassed | Should -Be 0 - } + $Result = Get-Karma + } - It 'should Invoke-Pester on koans until it fails a test' { - Assert-MockCalled Invoke-Koan -Times 1 - } + It 'produces a hashtable with data' { + $Result.PSTypeNames[0] | Should -Be 'PSKoans.Result' + $Result.KoansPassed | Should -Be 0 + } - It 'should populate the $script:CurrentTopic variable' { - $script:CurrentTopic | Should -BeOfType [hashtable] - $script:CurrentTopic.Count | Should -Be 4 - @('Name', 'Completed', 'Total', 'CurrentLine') | Should -BeIn $script:CurrentTopic.Keys - } + It 'calls Measure-Koan on each file to count koans' { + Should -Invoke 'Measure-Koan' -Scope Context -ModuleName 'PSKoans' } - Context 'With Nonexistent Koans Folder / No Koans Found' { - BeforeAll { - Mock Measure-Koan -ModuleName 'PSKoans' { } - Mock Update-PSKoan -ModuleName 'PSKoans' { throw 'Prevent recursion' } - Mock Write-Warning - } + It 'calls Invoke-Koan on each topic file until it fails a test' { + Should -Invoke 'Invoke-Koan' -Times 1 -ModuleName 'PSKoans' -Scope Context + } - It 'should attempt to populate koans and then recurse to reassess' { - { Get-Karma } | Should -Throw -ExpectedMessage 'Prevent recursion' - } + It 'populates the $script:CurrentTopic variable' { + $currentTopic = InModuleScope 'PSKoans' { $script:CurrentTopic } + $currentTopic | Should -BeOfType [hashtable] + $currentTopic.Count | Should -Be 4 + @('Name', 'Completed', 'Total', 'CurrentLine') | Should -BeIn $currentTopic.Keys + } + } - It 'should display a warning before initiating a reset' { - Assert-MockCalled Write-Warning - } + Context 'With Nonexistent Koans Folder / No Koans Found' { - It 'throws an error if a Topic is specified that matches nothing' { - { Get-Karma -Topic 'AboutAbsolutelyNothing' } | Should -Throw -ErrorId 'PSKoans.TopicNotFound' - } + BeforeAll { + Mock 'Measure-Koan' -ModuleName 'PSKoans' + Mock 'Get-PSKoan' -ParameterFilter { $Scope -eq 'User' } + Mock 'Update-PSKoan' { throw 'Prevent recursion' } + Mock 'Write-Warning' } - Context 'With -ListTopics Parameter' { - BeforeAll { - Mock Get-PSKoan { } - } + It 'should attempt to populate koans and then recurse to reassess' { + { Get-Karma } | Should -Throw -ExpectedMessage 'Prevent recursion' + Should -Invoke 'Update-PSKoan' -Scope Context + } - It 'should list all the koan topics' { - Get-Karma -ListTopics + It 'displays a warning before initiating a reset' { + Should -Invoke 'Write-Warning' -Scope Context + } - Assert-MockCalled Get-PSKoan - } + It 'throws an error if a Topic is specified that matches nothing' { + { Get-Karma -Topic 'AboutAbsolutelyNothing' } | Should -Throw -ErrorId 'PSKoans.TopicNotFound,Get-Karma' } + } - Context 'With -Topic Parameter' { - BeforeAll { - Mock Invoke-Koan -ModuleName 'PSKoans' { } - } + Context 'With -ListTopics Parameter' { - It 'should Invoke-Pester on only the topics selected: ' -TestCases @( - @{ Topic = @( 'AboutAssertions' ) } - @{ Topic = @( 'AboutArrays', 'AboutConditionals', 'AboutComparison' ) } - ) { - param([string[]] $Topic) + BeforeAll { + Mock 'Get-PSKoan' + } - Get-Karma -Topic $Topic + It 'lists all the koan topics' { + Get-Karma -ListTopics - Assert-MockCalled Invoke-Koan -Times @($Topic).Count - } + Should -Invoke 'Get-PSKoan' } + } - Context 'Behaviour When All Koans Are Completed' { - BeforeAll { - Mock Get-PSKoanLocation { - Join-Path -Path $TestDrive -ChildPath 'CompletedKoan' - } + Context 'With -Topic Parameter' { + + BeforeAll { + Mock 'Measure-Koan' -ModuleName 'PSKoans' { [int]::MaxValue } + Mock 'Invoke-Koan' -ModuleName 'PSKoans' + } - $TestFile = Join-Path -Path (Get-PSKoanLocation) -ChildPath 'Group\SelectedTopicTest.Koans.Ps1' - New-Item -Path (Split-Path $TestFile -Parent) -ItemType Directory -Force - Set-Content -Path $TestFile -Value @' - using module PSKoans - [Koan(Position = 1)] - param() + It 'calls Invoke-Koan on only the topics selected: ' -TestCases @( + @{ Topic = @( 'AboutAssertions' ) } + @{ Topic = @( 'AboutArrays', 'AboutConditionals', 'AboutComparison' ) } + ) { + Get-Karma -Topic $Topic - Describe 'Koans Test' { + Should -Invoke 'Invoke-Koan' -Times @($Topic).Count -ModuleName 'PSKoans' + } + } - It 'is easy to solve' { - $true | Should -BeTrue - } + Context 'Behaviour When All Koans Are Completed' { - It 'is positively trivial' { - $false | Should -BeFalse - } + BeforeAll { + Mock 'Get-PSKoanLocation' { + Join-Path -Path $TestDrive -ChildPath 'CompletedKoan' + } + + Mock 'Measure-Koan' -ModuleName 'PSKoans' -MockWith { 2 } + + $TestFile = Join-Path -Path (Get-PSKoanLocation) -ChildPath 'Group\SelectedTopicTest.Koans.ps1' + New-Item -Path (Split-Path $TestFile -Parent) -ItemType Directory -Force + Set-Content -Path $TestFile -Value @' + using module PSKoans + [Koan(Position = 1)] + param() + + Describe 'Koans Test' { + + It 'is easy to solve' { + $true | Should -BeTrue } -'@ - try { - $Result = Get-Karma -Topic SelectedTopicTest - } - catch { - # Ignore this. Error tests follow. + It 'is positively trivial' { + $false | Should -BeFalse + } } - } +'@ - It 'should not divide by zero if all Koans are completed' { - { Get-Karma -Topic SelectedTopicTest } | Should -Not -Throw + try { + $Result = Get-Karma -Topic SelectedTopicTest } - - It 'should output the result object' { - $Result | Should -Not -BeNullOrEmpty - $Result.PSTypeNames | Should -Contain 'PSKoans.CompleteResult' + catch { + # Ignore this. Error tests follow. } + } - It 'should indicate completion' { - $Result.Complete | Should -BeTrue - } + It 'does not divide by zero if all Koans are completed' { + { Get-Karma -Topic SelectedTopicTest } | Should -Not -Throw + } - It 'should indicate number of koans passed' { - $Result.KoansPassed | Should -Be 2 - } + It 'outputs the result object' { + $Result | Should -Not -BeNullOrEmpty + $Result.PSTypeNames | Should -Contain 'PSKoans.CompleteResult' + } - It 'should indicate total number of koans' { - $Result.TotalKoans | Should -Be 2 - } + It 'indicates completion' { + $Result.Complete | Should -BeTrue + } - It 'should indicate the requested topic' { - $Result.RequestedTopic | Should -Be 'SelectedTopicTest' - } + It 'indicates number of koans passed' { + $Result.KoansPassed | Should -Be 2 + } + + It 'indicates total number of koans' { + $Result.TotalKoans | Should -Be 2 + } + + It 'indicates the requested topic' { + $Result.RequestedTopic | Should -Be 'SelectedTopicTest' } } } diff --git a/Tests/Functions/Public/Get-PSKoan.Tests.ps1 b/Tests/Functions/Public/Get-PSKoan.Tests.ps1 index 0ac5a1db5..93bca3790 100644 --- a/Tests/Functions/Public/Get-PSKoan.Tests.ps1 +++ b/Tests/Functions/Public/Get-PSKoan.Tests.ps1 @@ -1,76 +1,63 @@ -#region Header -if (-not (Get-Module PSKoans)) { - $moduleBase = Join-Path -Path $psscriptroot.Substring(0, $psscriptroot.IndexOf('\Tests')) -ChildPath 'PSKoans' +#Requires -Modules PSKoans - Import-Module $moduleBase -Force -} -#endregion +Describe 'Get-PSKoan' { -InModuleScope PSKoans { - Describe 'Get-PSKoan' { - BeforeAll { - Mock Get-PSKoanLocation { - Join-Path $TestDrive 'PSKoans' - } + BeforeAll { + Mock 'Get-PSKoanLocation' { + Join-Path $TestDrive 'PSKoans' + } - Update-PSKoan -Confirm:$false + Update-PSKoan -Confirm:$false - # Stage test module - $path = Join-Path -Path (Get-PSKoanLocation) 'Modules\TestModule' - New-Item -Path $path -ItemType Directory -Force - Set-Content -Path (Join-Path -Path $path -ChildPath 'AboutSomething.Koans.ps1') -Value @' - using module PSKoans - [Koan(Position = 1, Module = 'TestModule')] - param( ) + # Stage test module + $path = Join-Path -Path (Get-PSKoanLocation) 'Modules\TestModule' + New-Item -Path $path -ItemType Directory -Force + Set-Content -Path (Join-Path -Path $path -ChildPath 'AboutSomething.Koans.ps1') -Value @' + using module PSKoans + [Koan(Position = 1, Module = 'TestModule')] + param() - Describe 'AboutSomething' { - It 'first' { - $true | Should -BeTrue - } + Describe 'AboutSomething' { + It 'first' { + $true | Should -BeTrue } + } '@ - } - - It 'should retrieve all except module-specific koan files' { - $Files = Get-ChildItem -Path (Get-PSKoanLocation) -Filter *.Koans.ps1 -Recurse -File | - Where-Object FullName -notmatch 'PSKoans[\\/]Modules[\\/]' - - (Get-PSKoan).Topic.Count | Should -Be $Files.Count - } + } - It 'should retrieve specific requested files with -Topic ' -TestCases @( - @{ Topic = 'AboutArrays' } - @{ Topic = 'AboutVariables' } - @{ Topic = 'AboutTypeOperators' } - @{ Topic = 'AboutLists' } - @{ Topic = 'AboutVariables', 'AboutLists' } - ) { - param($Topic) + It 'retrieves all except module-specific koan files' { + $Files = Get-ChildItem -Path (Get-PSKoanLocation) -Filter *.Koans.ps1 -Recurse -File | + Where-Object FullName -NotMatch 'PSKoans[\\/]Modules[\\/]' - (Get-PSKoan -Topic $Topic).Topic | Should -Be $Topic - } + (Get-PSKoan).Topic.Count | Should -Be $Files.Count + } - It 'should retrieve specific requested files with -Module ' -TestCases @( - @{ Topic = 'AboutSomething'; Module = 'TestModule' } - ) { - param($Topic, $Module) + It 'retrieves specific requested files with -Topic ' -TestCases @( + @{ Topic = 'AboutArrays' } + @{ Topic = 'AboutVariables' } + @{ Topic = 'AboutTypeOperators' } + @{ Topic = 'AboutLists' } + @{ Topic = 'AboutVariables', 'AboutLists' } + ) { + (Get-PSKoan -Topic $Topic).Topic | Should -Be $Topic + } - (Get-PSKoan -Module $Module -Scope User).Topic | Should -Be $Topic - } + It 'retrieves specific requested files with -Module ' -TestCases @( + @{ Topic = 'AboutSomething'; Module = 'TestModule' } + ) { + (Get-PSKoan -Module $Module -Scope User).Topic | Should -Be $Topic + } - if ($PSVersionTable.PSEdition -eq 'Desktop' -or $PSVersionTable.Platform -eq 'Win32NT') { - It 'should throw a terminating error if a file is blocked' { - $testFile = Get-ChildItem -Path (Get-PSKoanLocation) -Filter AboutArrays.Koans.ps1 -Recurse -File | - Select-Object -First 1 + It 'should throw a terminating error if a file is blocked' -Skip:($PSVersionTable.PSEdition -ne 'Desktop' -or $PSVersionTable.Platform -ne 'Win32NT') { + $testFile = Get-ChildItem -Path (Get-PSKoanLocation) -Filter AboutArrays.Koans.ps1 -Recurse -File | + Select-Object -First 1 - Set-Content -Path $testFile.FullName -Stream Zone.Identifier -Value @' + Set-Content -Path $testFile.FullName -Stream Zone.Identifier -Value @' [ZoneTransfer] ZoneId=3 ReferrerUrl=C:\Downloads\File.zip '@ - { Get-PSKoan -Topic AboutArrays -Scope User } | Should -Throw -ErrorId PSKoans.KoanFileIsBlocked - } - } + { Get-PSKoan -Topic AboutArrays -Scope User } | Should -Throw -ErrorId PSKoans.KoanFileIsBlocked } } diff --git a/Tests/Functions/Public/Get-PSKoanLocation.Tests.ps1 b/Tests/Functions/Public/Get-PSKoanLocation.Tests.ps1 index b80fe57d2..d62e5be02 100644 --- a/Tests/Functions/Public/Get-PSKoanLocation.Tests.ps1 +++ b/Tests/Functions/Public/Get-PSKoanLocation.Tests.ps1 @@ -1,23 +1,34 @@ #Requires -Modules PSKoans Describe 'Get-PSKoanLocation' { - BeforeAll { - Mock Get-PSKoanSetting -ModuleName PSKoans { - '~/PSKoans' - } -ParameterFilter { $Name -eq 'KoanLocation' } - } - It 'retrieves the koan library location' { - Get-PSKoanLocation | Should -Be '~/PSKoans' - } + Context 'Normal Behaviour' { + + BeforeAll { + Mock 'Get-PSKoanSetting' -ParameterFilter { $Name -eq 'KoanLocation' } -MockWith { + '~/PSKoans' + } - It 'calls Get-PSKoanSetting with -Name "LibraryFolder"' { - Assert-MockCalled Get-PSKoanSetting -ModuleName PSKoans + $Result = Get-PSKoanLocation + } + + It 'retrieves the koan library location' { + $Result | Should -Be '~/PSKoans' + } + + It 'calls Get-PSKoanSetting with -Name "KoanLocation"' { + Should -Invoke 'Get-PSKoanSetting' -Scope Context + } } - It 'throws an error if no value can be retrieved' { - Mock Get-PSKoanSetting -ModuleName PSKoans -ParameterFilter { $Name -eq 'KoanLocation' } + Context 'No Value Available' { + + BeforeAll { + Mock 'Get-PSKoanSetting' -ParameterFilter { $Name -eq 'KoanLocation' } + } - { Get-PSKoanLocation } | Should -Throw -ExpectedMessage 'location has not been defined' + It 'throws an error if no value can be retrieved' { + { Get-PSKoanLocation } | Should -Throw -ExpectedMessage 'PSKoans folder location has not been defined' + } } } diff --git a/Tests/Functions/Public/Get-PSKoanSetting.Tests.ps1 b/Tests/Functions/Public/Get-PSKoanSetting.Tests.ps1 index 220e7047c..e5b5e3efe 100644 --- a/Tests/Functions/Public/Get-PSKoanSetting.Tests.ps1 +++ b/Tests/Functions/Public/Get-PSKoanSetting.Tests.ps1 @@ -1,44 +1,65 @@ #Requires -Modules PSKoans Describe 'Get-PSKoanSetting' { + BeforeAll { - InModuleScope PSKoans { - $script:ConfigPath = 'TestDrive:/config.json' + $configFilePath = 'TestDrive:/config.json' + InModuleScope 'PSKoans' -Parameters @{ Path = $configFilePath } { + param($Path) + $script:OldConfigPath = $script:ConfigPath + $script:ConfigPath = $Path } + ${/} = [IO.Path]::DirectorySeparatorChar } + AfterAll { + InModuleScope 'PSKoans' { + $script:ConfigPath = $script:OldConfigPath + } + } + Context 'Settings file does not exist' { + BeforeAll { - Mock Set-PSKoanSetting { } -ParameterFilter { $Settings -is [hashtable] } -ModuleName PSKoans - $DefaultSettings = InModuleScope PSKoans { $script:DefaultSettings } + Mock 'Set-PSKoanSetting' -ParameterFilter { $Settings -is [hashtable] } + $DefaultSettings = InModuleScope 'PSKoans' { $script:DefaultSettings } } - +` It 'returns the default settings' { - $Result = InModuleScope PSKoans { Get-PSKoanSetting } + $Result = Get-PSKoanSetting $Result | Should -BeOfType [PSCustomObject] $Result.KoanLocation | Should -BeExactly "$HOME${/}PSKoans" $Result.Editor | Should -BeExactly 'code' } It 'calls Set-PSKoanSetting to set the default settings' { - Assert-MockCalled Set-PSKoanSetting -ModuleName PSKoans + Should -Invoke 'Set-PSKoanSetting' -Scope Context } } Context 'Settings file does exist' { + BeforeAll { - '{"KoanLocation": "TestLocation","Editor": "TestEditor"}' | Set-Content -Path 'TestDrive:/config.json' + [PSCustomObject]@{ + KoanLocation = "TestLocation" + Editor = "TestEditor" + } | + ConvertTo-Json | + Set-Content -Path $configFilePath } It 'returns all settings if none are specified' { - $Result = InModuleScope PSKoans { Get-PSKoanSetting } + $Result = Get-PSKoanSetting $Result.KoanLocation | Should -BeExactly 'TestLocation' $Result.Editor | Should -BeExactly 'TestEditor' } - It 'returns only the specified setting with -Name' { - InModuleScope PSKoans { Get-PSKoanSetting -Name KoanLocation } | Should -BeExactly 'TestLocation' + It 'returns only the specified setting with -Name ' -TestCases @( + @{ Name = 'KoanLocation'; Expected = 'TestLocation' } + @{ Name = 'Editor'; Expected = 'TestEditor' } + ) { + Get-PSKoanSetting -Name $Name | Should -BeExactly $Expected } } } diff --git a/Tests/Functions/Public/Move-PSKoanLibrary.Tests.ps1 b/Tests/Functions/Public/Move-PSKoanLibrary.Tests.ps1 index ed806df0d..4763e628e 100644 --- a/Tests/Functions/Public/Move-PSKoanLibrary.Tests.ps1 +++ b/Tests/Functions/Public/Move-PSKoanLibrary.Tests.ps1 @@ -9,9 +9,9 @@ Describe 'Move-PSKoanLibrary' { Select-Object -ExpandProperty FullName $TestPath = New-Item -ItemType Directory -Path 'TestDrive:/TestPath' | Join-Path -ChildPath 'Koans' - Mock Get-PSKoanLocation { $OriginalPath }.GetNewClosure() -ModuleName PSKoans - Mock Set-PSKoanLocation -ParameterFilter { $Path -eq $TestPath } -ModuleName PSKoans - Mock Move-Item -ParameterFilter { $Path -eq $OriginalPath } -MockWith { $Destination } -ModuleName PSKoans + Mock 'Get-PSKoanLocation' { $OriginalPath } + Mock 'Set-PSKoanLocation' -ParameterFilter { $Path -eq $TestPath } + Mock 'Move-Item' -ParameterFilter { $Path -eq $OriginalPath } -MockWith { $Destination } } It 'should output the new location' { @@ -19,15 +19,15 @@ Describe 'Move-PSKoanLibrary' { } It 'should call Get-PSKoanLocation' { - Assert-MockCalled Get-PSKoanLocation -ModuleName PSKoans + Should -Invoke 'Get-PSKoanLocation' -Scope Context } It 'should call Move-Item' { - Assert-MockCalled Move-Item -ModuleName PSKoans + Should -Invoke 'Move-Item' -Scope Context } It 'should call Set-PSKoanLocation' { - Assert-MockCalled Set-PSKoanLocation -ModuleName PSKoans + Should -Invoke 'Set-PSKoanLocation' -Scope Context } } @@ -39,16 +39,6 @@ Describe 'Move-PSKoanLibrary' { Set-PSKoanLocation -Path 'TestDrive:/PSKoans' Update-PSKoan -Confirm:$false - $OriginalFileHashes = Get-PSKoanLocation | - Get-ChildItem -Recurse -File | - Get-FileHash | - ForEach-Object { - @{ - File = ($_.Path -split [regex]::Escape([IO.Path]::DirectorySeparatorChar))[-2, -1] -join ':' - Hash = $_.Hash - } - } - $NewLocation = 'TestDrive:/NewLocation/PSKoans' New-Item -Path ($NewLocation | Split-Path -Parent) -ItemType Directory } @@ -66,16 +56,33 @@ Describe 'Move-PSKoanLibrary' { Get-PSKoanLocation | Should -BeExactly (Get-Item -Path $NewLocation).FullName } - It 'should copy to the new location' -TestCases $OriginalFileHashes { - param($File, $Hash) + It 'should copy all files intact to the new location' { + $OriginalFileHashes = Get-PSKoanLocation | + Get-ChildItem -Recurse -File | + Get-FileHash | + ForEach-Object { + @{ + File = ($_.Path -split [regex]::Escape([IO.Path]::DirectorySeparatorChar))[-2, -1] -join ':' + Hash = $_.Hash + } + } - $FileName = ($File -split ':')[-1] + $finalLocation = 'TestDrive:/FinalLocation/PSKoans' + New-Item -Path ($finalLocation | Split-Path -Parent) -ItemType Directory + Move-PSKoanLibrary -Path $finalLocation - $NewHash = Get-ChildItem -Path $NewLocation -Recurse -File -Filter "*$FileName*" | + $newFileHashes = @{} + Get-ChildItem -Path $finalLocation -Recurse -File -PipelineVariable Item | Get-FileHash | - Select-Object -ExpandProperty Hash + ForEach-Object { + $newFileHashes[$Item.Name] = $_.Hash + } - $NewHash | Should -BeExactly $Hash + foreach ($File in $OriginalFileHashes) { + $FileName = ($File.File -split ':')[-1] + $reason = "the hash of $($File.File) should match the new file hash" + $newFileHashes[$FileName] | Should -BeExactly $File.Hash -Because $reason + } } } diff --git a/Tests/Functions/Public/Register-Advice.Tests.ps1 b/Tests/Functions/Public/Register-Advice.Tests.ps1 index 55e907751..326a1931f 100644 --- a/Tests/Functions/Public/Register-Advice.Tests.ps1 +++ b/Tests/Functions/Public/Register-Advice.Tests.ps1 @@ -1,55 +1,62 @@ #Requires -Modules PSKoans +Describe "Register-Advice" { -InModuleScope 'PSKoans' { - Describe "Register-Advice" { - Context "Checkin the Behaviour if ProfileFolder and ProfilePath Not exists" { - BeforeAll{ - Mock Test-Path { $false } -ParameterFilter { $Path -eq $ProfileFolder } - Mock New-Item { } - Mock Test-Path { $false } -ParameterFilter { $Path -eq $ProfilePath } - Mock Set-Content { } -ParameterFilter { $Value -eq "Show-Advice" } - } - It "First Step Test-Path Fails and Create a Item" { - Register-Advice - Assert-MockCalled -CommandName Test-Path - } - It "Second Step Profile Path fails adding content in the file" { - Register-Advice - Assert-MockCalled -CommandName Set-Content -Times 1 - } + Context "Profile Folder/File Missing" { + + BeforeAll { + Mock New-Item -Verifiable + Mock Test-Path { $false } -Verifiable + Mock Set-Content -ParameterFilter { $Value -eq "Show-Advice" } -Verifiable } - Context "Checkin the Function with other Possible values" { - BeforeAll{ - Mock Test-Path { $true } -ParameterFilter { $Path -eq $ProfilePath } - Mock Select-String { $false } - Mock Add-Content { } - } - It "Third Step If file exists but not having the content (Get|Set)-Advice" { - Register-Advice - Assert-MockCalled -CommandName Add-Content - } + + It 'should create the $profile if it does not exist' { + Register-Advice + Should -InvokeVerifiable } - Context "Checkin the behaviour of function with different parameter values" { - BeforeAll{ - Mock Test-Path { $false } -ParameterFilter { $Path -eq $ProfileFolder } - Mock New-Item { } - Mock Test-Path { $false } -ParameterFilter { $Path -eq $ProfilePath } - Mock Set-Content { } -ParameterFilter { $Value -eq "Show-Advice" } - } - It "Should throw if invalid parameter(Targetprofile) value is passed" { - { Register-Advice -TargetProfile "Invalidvalue" } | Should -Throw - } - $testdata = @( - @{ ProfilePath = 'AllUsersAllHosts' } - @{ ProfilePath = 'AllUsersCurrentHost' } - @{ ProfilePath = 'CurrentUserAllHosts' } - @{ ProfilePath = 'CurrentUserCurrentHost' } - ) - It "Checking with the possible Parameter Values" -TestCases $testdata { - param($ProfilePath) - Register-Advice $ProfilePath | Should -BeNullOrEmpty - + } + + Context "Profile Already Exists" { + + BeforeAll { + Mock 'Test-Path' { $true } -Verifiable + Mock 'Select-String' { $false } -Verifiable + Mock 'Add-Content' -Verifiable + } + + It "adds content to the profile if it already exists (Get|Set)-Advice" { + Register-Advice + Should -InvokeVerifiable + } + } + + Context "Parameter Validation" { + + BeforeAll { + Mock Test-Path { $false } -ParameterFilter { $Path -eq $ProfileFolder } + Mock New-Item + Mock Test-Path { $false } -ParameterFilter { $Path -eq $ProfilePath } + Mock Set-Content -ParameterFilter { $Value -eq "Show-Advice" } + } + + It "throws if an invalid value is supplied for -TargetProfile" { + { Register-Advice -TargetProfile "Invalidvalue" } | Should -Throw + } + + It "works correctly with the profile" -TestCases @( + @{ ProfilePath = 'AllUsersAllHosts' } + @{ ProfilePath = 'AllUsersCurrentHost' } + @{ ProfilePath = 'CurrentUserAllHosts' } + @{ ProfilePath = 'CurrentUserCurrentHost' } + ) { + try { + Register-Advice $ProfilePath | Should -BeNullOrEmpty + } + catch [UnauthorizedAccessException] { + # Current user doesn't have access to the profile path. This can be normal for the 'AllUsers' paths. + if ($ProfilePath -notmatch '^AllUsers') { + throw $_ + } } } } diff --git a/Tests/Functions/Public/Reset-PSKoan.Tests.ps1 b/Tests/Functions/Public/Reset-PSKoan.Tests.ps1 index f0b33776a..885ab87c9 100644 --- a/Tests/Functions/Public/Reset-PSKoan.Tests.ps1 +++ b/Tests/Functions/Public/Reset-PSKoan.Tests.ps1 @@ -1,206 +1,211 @@ -#region Header -if (-not (Get-Module PSKoans)) { - $moduleBase = Join-Path -Path $psscriptroot.Substring(0, $psscriptroot.IndexOf('\Tests')) -ChildPath 'PSKoans' +#Requires -Modules PSKoans - Import-Module $moduleBase -Force -} -#endregion +Describe Reset-PSKoan { -InModuleScope PSKoans { - Describe Reset-PSKoan { - BeforeAll { - $defaultParams = @{ - Confirm = $false - } + BeforeAll { + $defaultParams = @{ + Confirm = $false + } - Mock Get-PSKoanLocation { - Join-Path -Path $TestDrive -ChildPath 'PSKoans' - } - Mock Get-PSKoan -ParameterFilter { $Scope -eq 'Module' } -MockWith { - [PSCustomObject]@{ - Topic = 'AboutSomething' - Path = Join-Path -Path $TestDrive -ChildPath 'Module\Group\AboutSomething.Koans.ps1' - RelativePath = 'Group\AboutSomething.Koans.ps1' - } + Mock 'Get-PSKoanLocation' { + Join-Path -Path $TestDrive -ChildPath 'PSKoans' + } + + Mock 'Get-PSKoan' -ParameterFilter { $Scope -eq 'Module' } -MockWith { + [PSCustomObject]@{ + Topic = 'AboutSomething' + Path = Join-Path -Path $TestDrive -ChildPath 'Module\Group\AboutSomething.Koans.ps1' + RelativePath = 'Group\AboutSomething.Koans.ps1' } - New-Item -Path (Join-Path -Path $TestDrive -ChildPath 'Module\Group') -ItemType Directory - New-Item -Path (Join-Path -Path $TestDrive -ChildPath 'PSKoans\Group') -ItemType Directory + } - $userFilePath = Get-PSKoanLocation | Join-Path -ChildPath 'Group\AboutSomething.Koans.ps1' + New-Item -Path (Join-Path -Path $TestDrive -ChildPath 'Module\Group') -ItemType Directory + New-Item -Path (Join-Path -Path $TestDrive -ChildPath 'PSKoans\Group') -ItemType Directory - Set-Content -Path (Get-PSKoan -Scope Module).Path, $userFilePath -Value @' - using module PSKoans - [Koan(Position = 1)] - param ( ) + $userFilePath = Get-PSKoanLocation | Join-Path -ChildPath 'Group\AboutSomething.Koans.ps1' - Describe 'AboutSomething' { - It 'existing content' { - __ | Should -Be 1 - } + Set-Content -Path (Get-PSKoan -Scope Module).Path, $userFilePath -Value @' + using module PSKoans + [Koan(Position = 1)] + param ( ) - It 'reset content' { - __ | Should -Be 2 - } + Describe 'AboutSomething' { + It 'existing content' { + __ | Should -Be 1 + } - Context 'first' { - It 'nested reset content' { - __ | Should -Be 3 - } + It 'reset content' { + __ | Should -Be 2 + } + + Context 'first' { + It 'nested reset content' { + __ | Should -Be 3 } + } - Context 'second' { - It 'nested reset content' { - __ | Should -Be 4 - } + Context 'second' { + It 'nested reset content' { + __ | Should -Be 4 } } + } '@ - } + } - Context 'User file exists, It block exists' { - BeforeAll { - Mock Set-Content - Mock Copy-Item - } + Context 'User file exists, It block exists' { - It 'When Name is supplied, updates an existing user file' { - Reset-PSKoan -Name 'existing content' @defaultParams + BeforeAll { + Mock 'Set-Content' + Mock 'Copy-Item' + } - Assert-MockCalled Set-Content -Times 1 -Scope It - Assert-MockCalled Copy-Item -Times 0 -Scope It - } + It 'updates an existing user file when -Name is supplied' { + Reset-PSKoan -Name 'existing content' @defaultParams - It 'When Context is supplied, updates an existing user file' { - Reset-PSKoan -Context 'first' @defaultParams + Should -Invoke 'Set-Content' -Times 1 + Should -Invoke 'Copy-Item' -Times 0 + } - Assert-MockCalled Set-Content -Times 1 -Exactly -Scope It - Assert-MockCalled Copy-Item -Times 0 -Scope It - } + It 'updates an existing user file when -Context is supplied' { + Reset-PSKoan -Context 'first' @defaultParams - It 'When Name and Context are supplied, updates an existing user file' { - Reset-PSKoan -Name 'nested reset content' -Context 'first' @defaultParams + Should -Invoke 'Set-Content' -Times 1 -Exactly + Should -Invoke 'Copy-Item' -Times 0 + } - Assert-MockCalled Set-Content -Times 1 -Exactly -Scope It - Assert-MockCalled Copy-Item -Times 0 -Scope It - } + It 'updates an existing user file when -Name and -Context are supplied' { + Reset-PSKoan -Name 'nested reset content' -Context 'first' @defaultParams - It 'When Name and Context are not supplied, copies a koan file from the module' { - Reset-PSKoan @defaultParams + Should -Invoke 'Set-Content' -Times 1 -Exactly + Should -Invoke 'Copy-Item' -Times 0 + } - Assert-MockCalled Set-Content -Times 0 -Scope It - Assert-MockCalled Copy-Item -Times 1 -Exactly -Scope It - } + It 'copies a koan file from the module when -Name and -Context are not supplied' { + Reset-PSKoan @defaultParams + + Should -Invoke 'Set-Content' -Times 0 + Should -Invoke 'Copy-Item' -Times 1 -Exactly } + } - Context 'User file exists, It block does not exist' { - BeforeAll { - Mock Get-KoanIt -ParameterFilter { $Path -match 'PSKoans' } - } + Context 'User file exists, It block does not exist' { - It 'When the user file does not include the specified Koan, writes a non-terminating error' { - { Reset-PSKoan -Topic AboutSomething -Name 'existing content' -ErrorAction Stop @defaultParams } | Should -Throw -ErrorId PSKoans.UserItNotFound - } + It 'writes a non-terminating error when the user file does not include the specified Koan' { + Mock 'Get-KoanIt' -ParameterFilter { $Path -match 'PSKoans' } -Module 'PSKoans' + + { Reset-PSKoan -Topic AboutSomething -Name 'existing content' -ErrorAction Stop @defaultParams } | + Should -Throw -ErrorId 'PSKoans.UserItNotFound,Reset-PSKoan' } + } - Context 'User file does not exist' { - BeforeAll { - New-Item "$TestDrive/DoesNotExist.Koans.ps1" -ItemType File > $null - Mock Get-PSKoan -ParameterFilter { $Scope -eq 'User' } - Mock Get-PSKoan -ParameterFilter { $Scope -eq 'Module' } { - [PSCustomObject]@{ - Topic = $Topic - Module = '_powershell' - Position = 101 - Path = "$TestDrive/DoesNotExist.Koans.ps1" - RelativePath = 'DoesNotExist.Koans.ps1' - PSTypeName = 'PSKoans.KoanInfo' - } + Context 'User file does not exist' { + + BeforeAll { + New-Item "$TestDrive/DoesNotExist.Koans.ps1" -ItemType File > $null + + Mock 'Get-PSKoan' -ParameterFilter { $Scope -eq 'User' } -Verifiable + Mock 'Get-PSKoan' -ParameterFilter { $Scope -eq 'Module' } -Verifiable -MockWith { + [PSCustomObject]@{ + Topic = $Topic + Module = '_powershell' + Position = 101 + Path = "$TestDrive/DoesNotExist.Koans.ps1" + RelativePath = 'DoesNotExist.Koans.ps1' + PSTypeName = 'PSKoans.KoanInfo' } - Mock Update-PSKoan } - It 'When the topic does not exist in the user location, call Update-PSKoan' { - Reset-PSKoan -Topic DoesNotExist -ErrorAction Stop @defaultParams + Mock 'Update-PSKoan' + } - Assert-MockCalled Update-PSKoan -Times 1 - } + It 'calls Update-PSKoan when the topic does not exist in the user location' { + Reset-PSKoan -Topic DoesNotExist -ErrorAction Stop @defaultParams + + Should -InvokeVerifiable + Should -Invoke Update-PSKoan -Times 1 -Exactly } + } - Context 'Module file does not exist' { - BeforeAll { - Mock Get-PSKoan -ParameterFilter { $Scope -eq 'Module' } - } + Context 'Module file does not exist' { - It 'When no topics are found in the module, throw a terminating error' { - { Reset-PSKoan -Topic DoesNotExist @defaultParams } | Should -Throw -ErrorId PSKoans.ModuleTopicNotFound - } + BeforeAll { + Mock 'Get-PSKoan' -ParameterFilter { $Scope -eq 'Module' } } - Context 'Practical tests' { - BeforeEach { - Set-Content -Path $userFilePath -Value @' - using module PSKoans - [Koan(Position = 1)] - param ( ) + It 'throws a terminating error when no topics are found in the module' { + { Reset-PSKoan -Topic DoesNotExist @defaultParams } | + Should -Throw -ErrorId 'PSKoans.ModuleTopicNotFound,Reset-PSKoan' - Describe 'AboutSomething' { - It 'existing content' { - 1 | Should -Be 1 - } + Should -Invoke 'Get-PSKoan' -Times 1 -Exactly + } + } - It 'reset content' { - 1 | Should -Be 2 - } + Context 'Practical tests' { - Context 'first' { - It 'nested reset content' { - 3 | Should -Be 3 - } + BeforeEach { + Set-Content -Path $userFilePath -Value @' + using module PSKoans + [Koan(Position = 1)] + param ( ) + + Describe 'AboutSomething' { + It 'existing content' { + 1 | Should -Be 1 + } + + It 'reset content' { + 1 | Should -Be 2 + } + + Context 'first' { + It 'nested reset content' { + 3 | Should -Be 3 } + } - Context 'second' { - It 'nested reset content' { - 4 | Should -Be 4 - } + Context 'second' { + It 'nested reset content' { + 4 | Should -Be 4 } } + } '@ - } + } - It 'should reset all koans in a file when Name is not specified' { - $userFilePath | Should -FileContentMatch '__ | Should -Be 1' - $userFilePath | Should -FileContentMatch '__ | Should -Be 2' - $userFilePath | Should -FileContentMatch '__ | Should -Be 3' - $userFilePath | Should -FileContentMatch '__ | Should -Be 4' - } + It 'should reset all koans in a file when Name is not specified' { + $userFilePath | Should -FileContentMatch '__ | Should -Be 1' + $userFilePath | Should -FileContentMatch '__ | Should -Be 2' + $userFilePath | Should -FileContentMatch '__ | Should -Be 3' + $userFilePath | Should -FileContentMatch '__ | Should -Be 4' + } - It 'should reset the state of a single koan without affecting others when Name is specified' { - Reset-PSKoan -Topic AboutSomething -Name 'reset content' @defaultParams + It 'should reset the state of a single koan without affecting others when Name is specified' { + Reset-PSKoan -Topic AboutSomething -Name 'reset content' @defaultParams - $userFilePath | Should -FileContentMatch '1 | Should -Be 1' - $userFilePath | Should -FileContentMatch '__ | Should -Be 2' - } + $userFilePath | Should -FileContentMatch '1 | Should -Be 1' + $userFilePath | Should -FileContentMatch '__ | Should -Be 2' + } - It 'supports context based searching' { - Reset-PSKoan -Topic AboutSomething -Name "nested reset content" -Context 'first' @defaultParams + It 'supports context based searching' { + Reset-PSKoan -Topic AboutSomething -Name "nested reset content" -Context 'first' @defaultParams - $userFilePath | Should -FileContentMatch '__ | Should -Be 3' - $userFilePath | Should -FileContentMatch '4 | Should -Be 4' - } + $userFilePath | Should -FileContentMatch '__ | Should -Be 3' + $userFilePath | Should -FileContentMatch '4 | Should -Be 4' + } - It 'allows koans of a given name to be reset across all contexts' { - Reset-PSKoan -Topic AboutSomething -Name "nested reset content" @defaultParams + It 'allows koans of a given name to be reset across all contexts' { + Reset-PSKoan -Topic AboutSomething -Name "nested reset content" @defaultParams - $userFilePath | Should -FileContentMatch '__ | Should -Be 3' - $userFilePath | Should -FileContentMatch '__ | Should -Be 4' - } + $userFilePath | Should -FileContentMatch '__ | Should -Be 3' + $userFilePath | Should -FileContentMatch '__ | Should -Be 4' + } - It 'supports wildcard patterns when matching name' { - Reset-PSKoan -Topic AboutSomething -Name "*content" @defaultParams + It 'supports wildcard patterns when matching name' { + Reset-PSKoan -Topic AboutSomething -Name "*content" @defaultParams - $userFilePath | Should -FileContentMatch '__ | Should -Be 1' - $userFilePath | Should -FileContentMatch '__ | Should -Be 2' - } + $userFilePath | Should -FileContentMatch '__ | Should -Be 1' + $userFilePath | Should -FileContentMatch '__ | Should -Be 2' } } } diff --git a/Tests/Functions/Public/Set-PSKoanLocation.Tests.ps1 b/Tests/Functions/Public/Set-PSKoanLocation.Tests.ps1 index f9a734d14..cac34f907 100644 --- a/Tests/Functions/Public/Set-PSKoanLocation.Tests.ps1 +++ b/Tests/Functions/Public/Set-PSKoanLocation.Tests.ps1 @@ -1,8 +1,9 @@ #Requires -Modules PSKoans Describe 'Set-PSKoanLocation' { + BeforeAll { - Mock Set-PSKoanSetting -ParameterFilter { $Name -eq 'KoanLocation' } -ModuleName PSKoans + Mock 'Set-PSKoanSetting' -ParameterFilter { $Name -eq 'KoanLocation' } } It 'outputs no data by default' { @@ -10,7 +11,7 @@ Describe 'Set-PSKoanLocation' { } It 'sets the KoanLocation setting' { - Assert-MockCalled Set-PSKoanSetting -ModuleName PSKoans + Should -Invoke 'Set-PSKoanSetting' -Scope Describe } It 'returns the input -Path value back to the pipeline with -PassThru' { diff --git a/Tests/Functions/Public/Set-PSKoanSetting.Tests.ps1 b/Tests/Functions/Public/Set-PSKoanSetting.Tests.ps1 index f657d8321..5feec692f 100644 --- a/Tests/Functions/Public/Set-PSKoanSetting.Tests.ps1 +++ b/Tests/Functions/Public/Set-PSKoanSetting.Tests.ps1 @@ -1,6 +1,7 @@ #Requires -Modules PSKoans Describe 'Set-PSKoanSetting' { + BeforeAll { InModuleScope 'PSKoans' { $script:OldConfigPath = $script:ConfigPath @@ -16,6 +17,7 @@ Describe 'Set-PSKoanSetting' { Context 'Settings file Exists' { Describe 'Setting values with -Name and -Value' { + BeforeAll { $NewConfigPath = InModuleScope 'PSKoans' { ($script:ConfigPath = "$TestDrive/config.json") @@ -25,28 +27,23 @@ Describe 'Set-PSKoanSetting' { New-Item -ItemType File -Path "$TestDrive/config.json" } - $Settings = [PSCustomObject]@{ + [PSCustomObject]@{ LibraryFolder = "$TestDrive/PSKoans" Editor = 'code' - } - $Settings | + } | ConvertTo-Json | Set-Content -Path $NewConfigPath - - $TestCases = @( - @{ Name = 'TestSetting1'; Value = 'TestValue1' } - @{ Name = 'LibraryFolder'; Value = "$TestDrive/AltLocation/PSKoans" } - @{ Name = 'Editor'; Value = 'code-insiders' } - ) } AfterAll { Remove-Item -Path $NewConfigPath -Force } - It 'should add a new setting: = ' -TestCases $TestCases { - param($Name, $Value) - + It 'should add a new setting: = ' -TestCases @( + @{ Name = 'TestSetting1'; Value = 'TestValue1' } + @{ Name = 'LibraryFolder'; Value = "TestDrive:/PSKoans" } + @{ Name = 'Editor'; Value = 'code-insiders' } + ) { Set-PSKoanSetting -Name $Name -Value $Value Get-PSKoanSetting -Name $Name | Should -BeExactly $Value @@ -58,6 +55,7 @@ Describe 'Set-PSKoanSetting' { } Context 'Setting values with -Settings Hashtable' { + BeforeAll { $NewConfigPath = InModuleScope 'PSKoans' { ($script:ConfigPath = "$TestDrive/config.json") @@ -67,11 +65,10 @@ Describe 'Set-PSKoanSetting' { New-Item -ItemType File -Path "$TestDrive/config.json" } - $Settings = [PSCustomObject]@{ + [PSCustomObject]@{ LibraryFolder = "$TestDrive/PSKoans" Editor = 'code' - } - $Settings | + } | ConvertTo-Json | Set-Content -Path $NewConfigPath } @@ -85,6 +82,7 @@ Describe 'Set-PSKoanSetting' { TestSetting2 = "TestValue2" LibraryFolder = "$TestDrive/PSKoans" } + Set-PSKoanSetting -Settings $NewSettings $Settings = Get-PSKoanSetting @@ -98,17 +96,13 @@ Describe 'Set-PSKoanSetting' { Context 'Settings file does not exist' { Describe 'Setting values with -Name and -Value' { + BeforeAll { $NewConfigPath = InModuleScope 'PSKoans' { ($script:ConfigPath = "$TestDrive/config.json") } $DefaultSettings = InModuleScope 'PSKoans' { $script:DefaultSettings } - $TestCases = @( - @{ Name = 'TestSetting1' ; Value = 'TestValue1' } - @{ Name = 'Editor'; Value = 'TestEditor' } - @{ Name = 'LibraryFolder'; Value = "$TestDrive/TestFolder" } - ) } BeforeEach { @@ -117,9 +111,11 @@ Describe 'Set-PSKoanSetting' { } } - It 'correctly adds the setting: = ' -TestCases $TestCases { - param($Name, $Value) - + It 'correctly adds the setting: = ' -TestCases @( + @{ Name = 'TestSetting1' ; Value = 'TestValue1' } + @{ Name = 'Editor'; Value = 'TestEditor' } + @{ Name = 'LibraryFolder'; Value = "$env:TEMP/TestFolder" } + ) { $NewConfigPath | Should -Not -Exist Set-PSKoanSetting -Name $Name -Value $Value $NewConfigPath | Should -Exist @@ -135,28 +131,25 @@ Describe 'Set-PSKoanSetting' { } Context 'Setting values with -Settings Hashtable' { + BeforeAll { $NewConfigPath = InModuleScope 'PSKoans' { ($script:ConfigPath = "$TestDrive/config.json") } $DefaultSettings = InModuleScope 'PSKoans' { $script:DefaultSettings } - $TestCases = @( - @{ Settings = @{ TestSetting1 = 'TestValue1'; Editor = 'TestEditor' } } - @{ Settings = @{ LibraryFolder = "$TestDrive/TestFolder"; TestSetting2 = 'TestValue2' } } - ) } BeforeEach { if (Test-Path -Path $NewConfigPath) { Remove-Item -Path $NewConfigPath } - } - It 'adds and replaces values: ' -TestCases $TestCases { - param($Settings) - + It 'adds and replaces values: ' -TestCases @( + @{ Settings = @{ TestSetting1 = 'TestValue1'; Editor = 'TestEditor' } } + @{ Settings = @{ LibraryFolder = "$TestDrive/TestFolder"; TestSetting2 = 'TestValue2' } } + ) { $NewConfigPath | Should -Not -Exist Set-PSKoanSetting -Settings $Settings $NewConfigPath | Should -Exist diff --git a/Tests/Functions/Public/Show-Advice.Tests.ps1 b/Tests/Functions/Public/Show-Advice.Tests.ps1 index fc8345992..e91fa726a 100644 --- a/Tests/Functions/Public/Show-Advice.Tests.ps1 +++ b/Tests/Functions/Public/Show-Advice.Tests.ps1 @@ -1,87 +1,82 @@ #Requires -Modules PSKoans -InModuleScope 'PSKoans' { - Describe "Show-Advice" { +Describe "Show-Advice" { - BeforeAll { - Mock Write-ConsoleLine { } - # Exporting incorrect advices to the folder - $AdviceFolder = $script:ModuleRoot | Join-Path -ChildPath 'Data/Advice' - $AdviceObject = Get-ChildItem -Path $AdviceFolder -Recurse -File -Filter "*.Advice.json" - $RandomAdvicesFilePaths = ($AdviceObject | Get-Random -Count 3).FullName - $Backup = $RandomAdvicesFilePaths | ForEach-Object { Get-Content $_ | ConvertFrom-Json } - } + BeforeAll { + Mock 'Write-ConsoleLine' -ModuleName 'PSKoans' + } + + Context "Behaviour of Parameter-less Calls" { - Context "Behaviour of Parameter-less Calls" { + BeforeAll { $result = Show-Advice + } - It "calls Write-ConsoleLine with Parameter -Title" { - Assert-MockCalled -CommandName Write-ConsoleLine -ParameterFilter { $null -eq $Title } - } + It "calls Write-ConsoleLine with Parameter -Title" { + Should -Invoke 'Write-ConsoleLine' -ModuleName 'PSKoans' -ParameterFilter { $null -eq $Title } -Scope Context + } - It "calls Write-ConsoleLine with only the display string" { - Assert-MockCalled -CommandName Write-ConsoleLine -ParameterFilter { $null -ne $Title } -Times 1 - } + It "calls Write-ConsoleLine with only the display string" { + Should -Invoke 'Write-ConsoleLine' -ModuleName 'PSKoans' -ParameterFilter { $null -ne $Title } -Scope Context + } - It "outputs nothing to the pipeline" { - $result | Should -BeNullOrEmpty - } + It "outputs nothing to the pipeline" { + $result | Should -BeNullOrEmpty } + } - Context "Behaviour with -Name Parameter" { + Context "Behaviour with -Name Parameter" { - It "should call Write-ConsoleLine with normal parameters" { - Show-Advice -Name "Profile" - Assert-MockCalled -CommandName Write-ConsoleLine -ParameterFilter { $null -ne $Title } - } + BeforeAll { + Show-Advice -Name "Profile" + } - It "should call Write-ConsoleLine without parameters" { - Show-Advice -Name "Profile" - Assert-MockCalled -CommandName Write-ConsoleLine -ParameterFilter { $null -eq $Title } - } + It "should call Write-ConsoleLine with normal parameters" { + Should -Invoke 'Write-ConsoleLine' -ParameterFilter { $null -ne $Title } -ModuleName 'PSKoans' -Scope Context + } - It "should throw an error if the requested file cannot be found" { - $message = "Could not find any Advice files matching the specified Name: ThisDoesntExist" - { Show-Advice -Name "ThisDoesntExist" -ErrorAction Stop } | Should -Throw -ExpectedMessage $Message - } + It "should call Write-ConsoleLine without parameters" { + Should -Invoke 'Write-ConsoleLine' -ParameterFilter { $null -eq $Title } -ModuleName 'PSKoans' -Scope Context } - Context 'Behaviour with malformed advice files' { - - BeforeAll { - $MalformedAdviceCases = @( - @{ - Json = @{ - NotTitle = "Fake title" - NotContent = @(1..4 | ForEach-Object { "Fake line $_" }) - } | ConvertTo-Json - } - @{ - Json = @{ - Content = @(1..4 | ForEach-Object { "Fake line $_" }) - } | ConvertTo-Json - } - @{ - Json = @{ - Title = "Fake title" - } | ConvertTo-Json - } - ) - - $GetContentResult = [string]::Empty - - Mock Get-Content -MockWith { $GetContentResult } - Mock Get-ChildItem -MockWith { [PSCustomObject]@{ PSPath = "DummyPath" } } - } + It "should throw an error if the requested file cannot be found" { + $message = "Could not find any Advice files matching the specified Name: ThisDoesntExist." + { Show-Advice -name "ThisDoesntExist" -ErrorAction Stop } | Should -Throw -ExpectedMessage $Message + } + } - It "should throw an error if the requested file's format is not correct" -TestCases $MalformedAdviceCases { - param($Json) + Context 'Behaviour with malformed advice files' { - $GetContentResult = $Json - $AdviceName = "TestAdvice" - $Message = "Could not find Title and/or Content elements for Advice file: {0}" -f $AdviceName - { Show-Advice -Name $AdviceName -ErrorAction Stop } | Should -Throw -ExpectedMessage $Message + BeforeAll { + $GetContentResult = [string]::Empty + + Mock Get-Content -MockWith { $GetContentResult } -Verifiable + Mock Get-ChildItem -MockWith { [PSCustomObject]@{ PSPath = "DummyPath" } } -Verifiable + } + + It "should throw an error if the requested file's format is not correct" -TestCases @( + @{ + Json = @{ + NotTitle = "Fake title" + NotContent = @(1..4 | ForEach-Object { "Fake line $_" }) + } | ConvertTo-Json + } + @{ + Json = @{ + Content = @(1..4 | ForEach-Object { "Fake line $_" }) + } | ConvertTo-Json + } + @{ + Json = @{ + Title = "Fake title" + } | ConvertTo-Json } + ) { + $GetContentResult = $Json + $AdviceName = "TestAdvice" + $Message = "Could not find Title and/or Content elements for Advice file: {0}" -f $AdviceName + { Show-Advice -name $AdviceName -ErrorAction Stop } | Should -Throw -ExpectedMessage $Message + Should -InvokeVerifiable } } } diff --git a/Tests/Functions/Public/Show-Karma.Tests.ps1 b/Tests/Functions/Public/Show-Karma.Tests.ps1 index 15259688a..f0bd6fb8e 100644 --- a/Tests/Functions/Public/Show-Karma.Tests.ps1 +++ b/Tests/Functions/Public/Show-Karma.Tests.ps1 @@ -1,15 +1,11 @@ -#region Header -if (-not (Get-Module PSKoans)) { - $moduleBase = Join-Path -Path $psscriptroot.Substring(0, $psscriptroot.IndexOf('\Tests')) -ChildPath 'PSKoans' - - Import-Module $moduleBase -Force -} -#endregion +#Requires -Modules PSKoans Describe 'Show-Karma' { + BeforeAll { - $StartingLocation = Get-PSKoanLocation - Set-PSKoanLocation -Path "$TestDrive/Koans" + Mock 'Get-PSKoanLocation' { + "$TestDrive/Koans" + } $EditorSetting = Get-PSKoanSetting -Name Editor @@ -17,355 +13,394 @@ Describe 'Show-Karma' { } AfterAll { - Set-PSKoanLocation -Path $StartingLocation Set-PSKoanSetting -Name Editor -Value $EditorSetting } - InModuleScope 'PSKoans' { - - Context 'Default Behaviour' { - BeforeAll { - Mock Out-Host { } - Mock Get-Karma -ModuleName 'PSKoans' { - [PSCustomObject]@{ - PSTypeName = 'PSKoans.Result' - Meditation = 'TestMeditation' - KoansPassed = 0 - TotalKoans = 400 - Describe = 'TestDescribe' - Expectation = 'ExpectedTest' - It = 'TestIt' - CurrentTopic = [PSCustomObject]@{ - Name = 'TestTopic"' - Completed = 0 - Total = 4 - CurrentLine = 1 - } + Context 'Default Behaviour' { + + BeforeAll { + Mock 'Out-Host' + Mock 'Get-Karma' { + [PSCustomObject]@{ + PSTypeName = 'PSKoans.Result' + Meditation = 'TestMeditation' + KoansPassed = 0 + TotalKoans = 400 + Describe = 'TestDescribe' + Expectation = 'ExpectedTest' + It = 'TestIt' + CurrentTopic = [PSCustomObject]@{ + Name = 'TestTopic"' + Completed = 0 + Total = 4 + CurrentLine = 1 } } } + } - It 'should not produce output' { - Show-Karma | Should -BeNullOrEmpty - } + It 'should not produce output' { + Show-Karma | Should -BeNullOrEmpty + } - It 'should write the formatted output to host' { - Assert-MockCalled Out-Host - } + It 'should write the formatted output to host' { + Should -Invoke 'Out-Host' -Scope Context + } - It 'should call Get-Karma to examine the koans' { - Assert-MockCalled Get-Karma - } + It 'should call Get-Karma to examine the koans' { + Should -Invoke 'Get-Karma' -Scope Context } + } - Context 'With All Koans Completed' { - BeforeAll { - Mock Out-Host { } - Mock Get-Karma -ModuleName 'PSKoans' { - [PSCustomObject]@{ - PSTypeName = 'PSKoans.CompleteResult' - KoansPassed = 10 - TotalKoans = 10 - RequestedTopci = $null - Complete = $true - } + Context 'With All Koans Completed' { + + BeforeAll { + Mock 'Out-Host' -Verifiable + Mock 'Get-Karma' -Verifiable { + [PSCustomObject]@{ + PSTypeName = 'PSKoans.CompleteResult' + KoansPassed = 10 + TotalKoans = 10 + RequestedTopci = $null + Complete = $true } } + } - It 'should not throw errors' { - { Show-Karma } | Should -Not -Throw - } + It 'should not throw errors' { + { Show-Karma } | Should -Not -Throw + Should -InvokeVerifiable } + } - Context 'With -ClearScreen Switch' { - BeforeAll { - Mock Clear-Host { } - Mock Out-Host { } - Mock Get-Karma -ModuleName 'PSKoans' { - [PSCustomObject]@{ - PSTypeName = 'PSKoans.Result' - Meditation = 'TestMeditation' - KoansPassed = 0 - TotalKoans = 400 - Describe = 'TestDescribe' - Expectation = 'ExpectedTest' - It = 'TestIt' - CurrentTopic = [PSCustomObject]@{ - Name = 'TestTopic"' - Completed = 0 - Total = 4 - CurrentLine = 1 - } + Context 'With -ClearScreen Switch' { + + BeforeAll { + Mock 'Clear-Host' + Mock 'Out-Host' + Mock 'Get-Karma' { + [PSCustomObject]@{ + PSTypeName = 'PSKoans.Result' + Meditation = 'TestMeditation' + KoansPassed = 0 + TotalKoans = 400 + Describe = 'TestDescribe' + Expectation = 'ExpectedTest' + It = 'TestIt' + CurrentTopic = [PSCustomObject]@{ + Name = 'TestTopic"' + Completed = 0 + Total = 4 + CurrentLine = 1 } } } + } - It 'should not produce output' { - Show-Karma -ClearScreen | Should -Be $null - } + It 'should not produce output' { + Show-Karma -ClearScreen | Should -Be $null + } - It 'should clear the screen' { - Assert-MockCalled Clear-Host -Times 1 - } + It 'should clear the screen' { + Should -Invoke 'Clear-Host' -Scope Context -Times 1 -Exactly + } - It 'should display the rendered output' { - Assert-MockCalled Out-Host - } + It 'should display the rendered output' { + Should -Invoke 'Out-Host' -Scope Context + } - It 'should Invoke-Pester on each of the koans' { - Assert-MockCalled Get-Karma - } + It 'should use Get-Karma to retrieve koan results' { + Should -Invoke 'Get-Karma' -Scope Context -Times 1 -Exactly } + } - Context 'With Nonexistent Koans Folder / No Koans Found' { - BeforeAll { - Mock Out-Host { } - Mock Get-PSKoan -ModuleName 'PSKoans' { } - Mock Update-PSKoan -ModuleName 'PSKoans' { throw 'Prevent recursion' } - Mock Write-Warning - Mock Test-Path { $false } - Mock Invoke-Item - Mock New-Item - } + Context 'With Nonexistent Koans Folder / No Koans Found' { - BeforeEach { - $script:CurrentTopic = $null - } + BeforeAll { + Mock 'Write-Host' + Mock 'Get-PSKoan' + Mock 'Update-PSKoan' { throw 'Prevent recursion' } + Mock 'Write-Warning' + Mock 'Test-Path' { $false } + Mock 'Invoke-Item' + Mock 'Measure-Koan' -ModuleName 'PSKoans' + } - It 'should attempt to populate koans and then recurse to reassess' { - { Show-Karma } | Should -Throw -ExpectedMessage 'Prevent recursion' - } + BeforeEach { + InModuleScope 'PSKoans' { $script:CurrentTopic = $null } + } - It 'should display a warning before initiating a reset' { - Assert-MockCalled Write-Warning - } + It 'should attempt to populate koans and then recurse to reassess' { + { Show-Karma } | Should -Throw -ExpectedMessage 'Prevent recursion' + } - It 'throws an error if a Topic is specified that matches nothing' { - { Show-Karma -Topic 'AboutAbsolutelyNothing' } | Should -Throw -ErrorId 'PSKoans.TopicNotFound' - } + It 'should display a warning before initiating a reset' { + Should -Invoke 'Write-Warning' -Scope Context -Times 1 -Exactly + } - It 'should create PSKoans directory with -Library' { - { Show-Karma -Library } | Should -Throw -ExpectedMessage 'Prevent recursion' + It 'throws an error if a Topic is specified that matches nothing' { + { Show-Karma -Topic 'AboutAbsolutelyNothing' } | Should -Throw -ErrorId 'PSKoans.TopicNotFound,Show-Karma' + } - Assert-MockCalled Test-Path -Times 1 - Assert-MockCalled Update-PSKoan -Times 1 - Assert-MockCalled New-Item -Times 1 - } + It 'should create PSKoans directory with -Library' { + { Show-Karma -Library } | Should -Throw -ExpectedMessage 'Prevent recursion' - It 'should create PSKoans directory with -Contemplate' { - { Show-Karma -Contemplate } | Should -Throw -ExpectedMessage 'Prevent recursion' + Should -Invoke 'Test-Path' + Should -Invoke 'Update-PSKoan' -Times 1 -Exactly + } - Assert-MockCalled Test-Path -Times 1 - Assert-MockCalled Update-PSKoan -Times 1 - Assert-MockCalled New-Item -Times 1 - } + It 'should call Get-PSKoan to retrieve the correct file -Contemplate' { + { Show-Karma -Contemplate } | Should -Throw -ExpectedMessage 'Prevent recursion' + + Should -Invoke 'Get-PSKoan' -Times 1 -Exactly + Should -Invoke 'Update-PSKoan' -Times 1 -Exactly } + } - Context 'With -ListTopics Parameter' { - BeforeAll { - Mock Get-PSKoan - } + Context 'With -ListTopics Parameter' { - It 'should list all the koan topics' { - Show-Karma -ListTopics - Assert-MockCalled Get-PSKoan - } + BeforeAll { + Mock 'Get-PSKoan' } - Context 'With -Topic Parameter' { - BeforeAll { - Mock Out-Host { } - Mock Get-Karma -MockWith { - [PSCustomObject]@{ - PSTypeName = 'PSKoans.Result' - Meditation = 'TestMeditation' - KoansPassed = 0 - TotalKoans = 400 - Describe = 'TestDescribe' - Expectation = 'ExpectedTest' - It = 'TestIt' - CurrentTopic = [PSCustomObject]@{ - Name = 'TestTopic"' - Completed = 0 - Total = 4 - CurrentLine = 1 - } - RequestedTopic = $Topic + It 'should list all the koan topics' { + Show-Karma -ListTopics + Should -Invoke 'Get-PSKoan' -Times 1 -Exactly + } + } + + Context 'With -Topic Parameter' { + + BeforeAll { + Mock 'Out-Host' -Verifiable + Mock 'Get-Karma' -ParameterFilter { $Topic -eq 'TestTopic' } -Verifiable -MockWith { + [PSCustomObject]@{ + PSTypeName = 'PSKoans.Result' + Meditation = 'TestMeditation' + KoansPassed = 0 + TotalKoans = 400 + Describe = 'TestDescribe' + Expectation = 'ExpectedTest' + It = 'TestIt' + CurrentTopic = [PSCustomObject]@{ + Name = 'TestTopic"' + Completed = 0 + Total = 4 + CurrentLine = 1 } - } -ParameterFilter { $Topic } + RequestedTopic = $Topic + } } + } - It 'should call Get-Karma on the selected topic' { - Show-Karma -Topic TestTopic - Assert-MockCalled Get-Karma -ParameterFilter { $Topic -eq "TestTopic" } - } + It 'should call Get-Karma on the selected topic' { + Show-Karma -Topic TestTopic + Should -InvokeVerifiable } + } - Context 'With All Koans in a Single Topic Completed' { - BeforeAll { - Mock Format-Custom { $InputObject.Complete } - Mock Out-Host { } - Mock Get-Karma -ModuleName 'PSKoans' { - [PSCustomObject]@{ - PSTypeName = 'PSKoans.CompleteResult' - KoansPassed = 10 - TotalKoans = 10 - RequestedTopic = 'TestTopic' - Complete = $true - } + Context 'With All Koans in a Single Topic Completed' { + + BeforeAll { + Mock 'Format-Custom' -Verifiable { $null } + Mock 'Out-Host' -Verifiable + Mock 'Get-Karma' -Verifiable { + [PSCustomObject]@{ + PSTypeName = 'PSKoans.CompleteResult' + KoansPassed = 10 + TotalKoans = 10 + RequestedTopic = 'TestTopic' + Complete = $true } } + } - It 'should not throw errors' { - { Show-Karma } | Should -Not -Throw - } + It 'should not throw errors' { + { Show-Karma } | Should -Not -Throw + Should -InvokeVerifiable } + } - Context 'With -Contemplate Switch' { - BeforeAll { - $TestFile = New-TemporaryFile + Context 'With -Contemplate Switch' { - Mock Invoke-Item { $Path } - Mock Get-Command { $true } -ParameterFilter { $Name -ne "missing_editor" } - Mock Get-Command { $false } -ParameterFilter { $Name -eq "missing_editor" } - Mock Start-Process { - @{ Editor = $FilePath; Arguments = $ArgumentList; NoNewWindow = $NoNewWindow } + BeforeAll { + $TestFile = New-TemporaryFile + + Mock 'Invoke-Item' { $Path } + Mock 'Get-Command' { $true } -ParameterFilter { $Name -ne "missing_editor" } + Mock 'Get-Command' { $false } -ParameterFilter { $Name -eq "missing_editor" } + Mock 'Start-Process' { + @{ Editor = $FilePath; Arguments = $ArgumentList; NoNewWindow = $NoNewWindow } + } + + Mock 'Get-Karma' { + $currentTopic = @{ + Name = 'TestTopic' + Completed = 0 + Total = 4 + CurrentLine = 1 } - Mock Get-Karma -ModuleName 'PSKoans' { - $script:CurrentTopic = @{ - Name = 'TestTopic' - Completed = 0 - Total = 4 - CurrentLine = 1 - } - [PSCustomObject]@{ - PSTypeName = 'PSKoans.Result' - Meditation = 'TestMeditation' - KoansPassed = 0 - TotalKoans = 400 - Describe = 'TestDescribe' - Expectation = 'ExpectedTest' - It = 'TestIt' - CurrentTopic = [PSCustomObject]$script:CurrentTopic - } + [PSCustomObject]@{ + PSTypeName = 'PSKoans.Result' + Meditation = 'TestMeditation' + KoansPassed = 0 + TotalKoans = 400 + Describe = 'TestDescribe' + Expectation = 'ExpectedTest' + It = 'TestIt' + CurrentTopic = [PSCustomObject]$currentTopic + } + + InModuleScope 'PSKoans' -Parameters @{ Topic = $currentTopic } { + param($Topic) + $script:CurrentTopic = $Topic } - Mock Get-PSKoan -ModuleName 'PSKoans' { - [PSCustomObject]@{ Path = $TestFile.FullName } - } -ParameterFilter { $Topic -eq 'TestTopic' } } - AfterAll { - $TestFile | Remove-Item + Mock 'Get-PSKoan' -ParameterFilter { $Scope -eq 'User' } { + [PSCustomObject]@{ Path = $TestFile.FullName } } + } + + AfterAll { + $TestFile | Remove-Item + } - It 'invokes VS Code with "code" set as Editor with proper arguments' { - Set-PSKoanSetting -Name Editor -Value 'code' - $Result = Show-Karma -Contemplate + It 'invokes VS Code with "code" set as Editor with proper arguments' { + Set-PSKoanSetting -Name Editor -Value 'code' + $Result = Show-Karma -Contemplate - $Result.Editor | Should -BeExactly 'code' - $Result.Arguments[0] | Should -BeExactly '--goto' - $Result.Arguments[1] | Should -MatchExactly '"[^"]+":\d+' - $Result.Arguments[2] | Should -BeExactly '--reuse-window' - $Result.NoNewWindow | Should -BeTrue + $Result.Editor | Should -BeExactly 'code' + $Result.Arguments[0] | Should -BeExactly '--goto' + $Result.Arguments[1] | Should -MatchExactly '"[^"]+":\d+' + $Result.Arguments[2] | Should -BeExactly '--reuse-window' + $Result.NoNewWindow | Should -BeTrue - # Resolve-Path doesn't like embedded quotes - $Path = ($Result.Arguments[1] -split '(?<="):')[0] -replace '"' - $Path | Should -BeExactly (Resolve-Path -Path $Path).Path + # Resolve-Path doesn't like embedded quotes + $Path = ($Result.Arguments[1] -split '(?<="):')[0] -replace '"' + $Path | Should -BeExactly (Resolve-Path -Path $Path).Path - Assert-MockCalled Get-Command -Times 1 - Assert-MockCalled Start-Process -Times 1 + Should -Invoke 'Get-Command' -Times 1 -Exactly + Should -Invoke 'Start-Process' -Times 1 -Exactly - $script:CurrentTopic | Should -BeNullOrEmpty - } + InModuleScope 'PSKoans' { $script:CurrentTopic } | Should -BeNullOrEmpty + } - It 'opens the specified -Topic in the selected editor' { - Set-PSKoanSetting -Name Editor -Value 'code' - $Result = Show-Karma -Contemplate -Topic TestTopic - $Result.Arguments[1] | Should -MatchExactly ([regex]::Escape($TestFile.FullName)) + $moduleCases = @( + @{ ModuleName = 'ActiveDirectory' } + @{ ModuleName = 'dbatools' } + ) + It 'opens the selected editor targeting koans for the module' -TestCases $moduleCases { + Set-PSKoanSetting -Name Editor -Value 'code' + $Result = Show-Karma -Contemplate -Module $ModuleName - Assert-MockCalled Get-Command -Times 1 - Assert-MockCalled Start-Process -Times 1 + $Result.Editor | Should -BeExactly 'code' + $Result.Arguments[0] | Should -BeExactly '--goto' + $Result.Arguments[1] | Should -MatchExactly '"[^"]+":\d+' + $Result.Arguments[2] | Should -BeExactly '--reuse-window' + $Result.NoNewWindow | Should -BeTrue - $script:CurrentTopic | Should -BeNullOrEmpty - } + # Resolve-Path doesn't like embedded quotes + $Path = ($Result.Arguments[1] -split '(?<="):')[0] -replace '"' + $Path | Should -BeExactly (Resolve-Path -Path $Path).Path - It 'invokes the set editor with unknown editor chosen' { - Set-PSKoanSetting -Name Editor -Value 'vim' + Should -Invoke 'Get-Command' -Times 1 -Exactly + Should -Invoke 'Start-Process' -Times 1 -Exactly + Should -Invoke 'Get-Karma' -ParameterFilter { $Module -eq $ModuleName } + Should -Invoke 'Get-PSKoan' -ParameterFilter { $IncludeModule -eq $ModuleName } - $Result = Show-Karma -Contemplate - $Result.Editor | Should -BeExactly 'vim' - $Result.Arguments | Should -MatchExactly '"[^"]+"' + InModuleScope 'PSKoans' { $script:CurrentTopic } | Should -BeNullOrEmpty + } - # Resolve-Path doesn't like embedded quotes - $Path = $Result.Arguments -replace '"' - $Path | Should -BeExactly (Resolve-Path -Path $Path).Path + It 'opens the specified -Topic in the selected editor' { + Set-PSKoanSetting -Name Editor -Value 'code' - Assert-MockCalled Get-Command -Times 1 - Assert-MockCalled Start-Process -Times 1 + $Result = Show-Karma -Contemplate -Topic TestTopic + $Result.Arguments[1] | Should -MatchExactly ([regex]::Escape($TestFile.FullName)) - $script:CurrentTopic | Should -BeNullOrEmpty - } + Should -Invoke 'Get-Command' -Times 1 -Exactly + Should -Invoke 'Start-Process' -Times 1 -Exactly - It 'opens the file directly when selected editor is unavailable' { - Set-PSKoanSetting -Name Editor -Value "missing_editor" + InModuleScope 'PSKoans' { $script:CurrentTopic } | Should -BeNullOrEmpty + } - Show-Karma -Contemplate | Should -BeExactly $TestFile.FullName + It 'invokes the set editor with unknown editor chosen' { + Set-PSKoanSetting -Name Editor -Value 'vim' - Assert-MockCalled Get-Command -Times 1 -ParameterFilter { $Name -eq "missing_editor" } - Assert-MockCalled Invoke-Item -Times 1 + $Result = Show-Karma -Contemplate + $Result.Editor | Should -BeExactly 'vim' + $Result.Arguments | Should -MatchExactly '"[^"]+"' - $script:CurrentTopic | Should -BeNullOrEmpty - } + # Resolve-Path doesn't like embedded quotes + $Path = $Result.Arguments -replace '"' + $Path | Should -BeExactly (Resolve-Path -Path $Path).Path + + Should -Invoke 'Get-Command' -Times 1 -Exactly + Should -Invoke 'Start-Process' -Times 1 -Exactly + + InModuleScope 'PSKoans' { $script:CurrentTopic } | Should -BeNullOrEmpty } - Context 'With -Library Switch' { - BeforeAll { - Mock Get-Command { $true } -ParameterFilter { $Name -ne "missing_editor" } - Mock Get-Command { $false } -ParameterFilter { $Name -eq "missing_editor" } - Mock Start-Process { - @{ Editor = $FilePath; Arguments = $ArgumentList } - } - Mock Invoke-Item { $Path } - } + It 'opens the file directly when selected editor is unavailable' { + Set-PSKoanSetting -name Editor -Value "missing_editor" - It 'invokes VS Code with "code" set as Editor with proper arguments' { - Set-PSKoanSetting -Name Editor -Value 'code' - $Result = Show-Karma -Library + Show-Karma -Contemplate | Should -BeExactly $TestFile.FullName - $Result.Editor | Should -BeExactly 'code' + Should -Invoke 'Get-Command' -Times 1 -Exactly -ParameterFilter { $Name -eq "missing_editor" } + Should -Invoke 'Invoke-Item' -Times 1 -Exactly - # Resolve-Path doesn't like embedded quotes - $Path = $Result.Arguments -replace '"' - $Path | Should -BeExactly (Resolve-Path -Path $Path).Path + InModuleScope 'PSKoans' { $script:CurrentTopic } | Should -BeNullOrEmpty + } + } + + Context 'With -Library Switch' { - Assert-MockCalled Get-Command -Times 1 - Assert-MockCalled Start-Process -Times 1 + BeforeAll { + Mock 'Get-Command' { $true } -ParameterFilter { $Name -ne "missing_editor" } + Mock 'Get-Command' { $false } -ParameterFilter { $Name -eq "missing_editor" } + Mock 'Start-Process' { + @{ Editor = $FilePath; Arguments = $ArgumentList } } + Mock 'Invoke-Item' { $Path } + } - It 'invokes the set editor with unknown editor chosen' { - Set-PSKoanSetting -Name Editor -Value 'vim' + It 'invokes VS Code with "code" set as Editor with proper arguments' { + Set-PSKoanSetting -Name Editor -Value 'code' - $Result = Show-Karma -Library - $Result.Editor | Should -BeExactly 'vim' + $Result = Show-Karma -Library + $Result.Editor | Should -BeExactly 'code' - # Resolve-Path doesn't like embedded quotes - $Path = $Result.Arguments -replace '"' - $Path | Should -BeExactly (Resolve-Path -Path $Path).Path + # Resolve-Path doesn't like embedded quotes + $Path = $Result.Arguments -replace '"' + $Path | Should -BeExactly (Resolve-Path -Path $Path).Path - Assert-MockCalled Get-Command -Times 1 - Assert-MockCalled Start-Process -Times 1 - } + Should -Invoke 'Get-Command' -Times 1 -Exactly + Should -Invoke 'Start-Process' -Times 1 -Exactly + } - It 'opens the file directly when selected editor is unavailable' { - Set-PSKoanSetting -Name Editor -Value "missing_editor" + It 'invokes the set editor with unknown editor chosen' { + Set-PSKoanSetting -Name Editor -Value 'vim' - Show-Karma -Library | Should -BeExactly (Get-PSKoanLocation) + $Result = Show-Karma -Library + $Result.Editor | Should -BeExactly 'vim' - Assert-MockCalled Get-Command -Times 1 -ParameterFilter { $Name -eq "missing_editor" } - Assert-MockCalled Invoke-Item -Times 1 - } + # Resolve-Path doesn't like embedded quotes + $Path = $Result.Arguments -replace '"' + $Path | Should -BeExactly (Resolve-Path -Path $Path).Path + + Should -Invoke 'Get-Command' -Times 1 -Exactly + Should -Invoke 'Start-Process' -Times 1 -Exactly + } + + It 'opens the file directly when selected editor is unavailable' { + Set-PSKoanSetting -name Editor -Value "missing_editor" + + Show-Karma -Library | Should -BeExactly (Get-PSKoanLocation) + + Should -Invoke 'Get-Command' -Times 1 -Exactly -ParameterFilter { $Name -eq "missing_editor" } + Should -Invoke 'Invoke-Item' -Times 1 -Exactly } } } diff --git a/Tests/Functions/Public/Update-PSKoan.Tests.ps1 b/Tests/Functions/Public/Update-PSKoan.Tests.ps1 index 8806c56c8..d86cf7f29 100644 --- a/Tests/Functions/Public/Update-PSKoan.Tests.ps1 +++ b/Tests/Functions/Public/Update-PSKoan.Tests.ps1 @@ -1,115 +1,109 @@ -#region Header -if (-not (Get-Module PSKoans)) { - $moduleBase = Join-Path -Path $psscriptroot.Substring(0, $psscriptroot.IndexOf('\Tests')) -ChildPath 'PSKoans' +#Requires -Modules PSKoans - Import-Module $moduleBase -Force -} -#endregion +Describe 'Update-PSKoan' { + + Context 'Mocked Commands' { -InModuleScope 'PSKoans' { - Describe 'Update-PSKoan' { BeforeAll { - Mock Get-PSKoanLocation { - Join-Path -Path $TestDrive -ChildPath 'PSKoans' + Mock 'Remove-Item' + Mock 'Copy-Item' + Mock 'New-Item' + Mock 'Move-Item' + Mock 'Update-PSKoanFile' -ModuleName 'PSKoans' + + Mock 'Get-PSKoan' -ParameterFilter { $Scope -eq 'Module' } -MockWith { + [PSCustomObject]@{ + Topic = 'Missing' + Path = 'Module\Group\AboutSomethingMissing.Koans.ps1' + } + [PSCustomObject]@{ + Topic = 'IncorrectPath' + Path = 'Module\Group\AboutSomethingIncorrectPath.Koans.ps1' + } + [PSCustomObject]@{ + Topic = 'Existing' + Path = 'Module\Group\AboutSomethingExisting.Koans.ps1' + } } - New-Item -Path (Get-PSKoanLocation) -ItemType Directory - } - Context 'Mocked Commands' { - BeforeAll { - Mock Remove-Item - Mock Copy-Item - Mock New-Item - Mock Move-Item - Mock Update-PSKoanFile - - Mock Get-PSKoan -ParameterFilter { $Scope -eq 'Module' } -MockWith { - [PSCustomObject]@{ - Topic = 'Missing' - Path = 'Module\Group\AboutSomethingMissing.Koans.ps1' - } - [PSCustomObject]@{ - Topic = 'IncorrectPath' - Path = 'Module\Group\AboutSomethingIncorrectPath.Koans.ps1' - } - [PSCustomObject]@{ - Topic = 'Existing' - Path = 'Module\Group\AboutSomethingExisting.Koans.ps1' - } + Mock 'Get-PSKoan' -ParameterFilter { $Scope -eq 'User' } -MockWith { + [PSCustomObject]@{ + Topic = 'IncorrectPath' + Path = 'Module\RetiredGroup\AboutSomethingIncorrectPath.Koans.ps1' } - Mock Get-PSKoan -ParameterFilter { $Scope -eq 'User' } -MockWith { - [PSCustomObject]@{ - Topic = 'IncorrectPath' - Path = 'Module\RetiredGroup\AboutSomethingIncorrectPath.Koans.ps1' - } - [PSCustomObject]@{ - Topic = 'RetiredTopic' - Path = 'Module\RetiredGroup\AboutSomethingRetiredTopic.Koans.ps1' - } - [PSCustomObject]@{ - Topic = 'Existing' - Path = 'Module\Group\AboutSomethingExisting.Koans.ps1' - } + [PSCustomObject]@{ + Topic = 'RetiredTopic' + Path = 'Module\RetiredGroup\AboutSomethingRetiredTopic.Koans.ps1' + } + [PSCustomObject]@{ + Topic = 'Existing' + Path = 'Module\Group\AboutSomethingExisting.Koans.ps1' } } + } - It 'should not produce output' { - Update-PSKoan -Confirm:$false | Should -BeNullOrEmpty - } + It 'should not produce output' { + Update-PSKoan -Confirm:$false | Should -BeNullOrEmpty + } - It 'should copy missing topic files' { - Assert-MockCalled Copy-Item -Times 1 - } + It 'should copy missing topic files' { + Should -Invoke 'Copy-Item' -Times 1 -Scope Context + } - It 'should move incorrectly placed topics' { - Assert-MockCalled Remove-Item -Times 1 - } + It 'should move incorrectly placed topics' { + Should -Invoke 'Remove-Item' -Times 1 -Scope Context + } - It 'should remove discarded topics' { - Assert-MockCalled Remove-Item -Times 1 - } + It 'should remove discarded topics' { + Should -Invoke 'Remove-Item' -Times 1 -Scope Context + } - It 'should update topics which exist in module and koan path' { - Assert-MockCalled Update-PSKoanFile -Times 2 - } + It 'should update topics which exist in module and koan path' { + Should -Invoke 'Update-PSKoanFile' -ModuleName 'PSKoans' -Times 2 -Scope Context } + } - Context 'Practical Tests with TestDrive' { - BeforeAll { - Update-PSKoan -Confirm:$false + Context 'Practical Tests with TestDrive' { - $file = Get-ChildItem -Path (Get-PSKoanLocation) -Filter *.koans.ps1 -File -Recurse | - Select-Object -First 1 + BeforeAll { + Mock 'Get-PSKoanLocation' { + Join-Path -Path $TestDrive -ChildPath 'PSKoans' } - It 'should copy missing topic files' { - $file | Remove-Item - $file.FullName | Should -Not -Exist + New-Item -Path (Get-PSKoanLocation) -ItemType Directory + Update-PSKoan -Confirm:$false - Update-PSKoan -Confirm:$false + $file = Get-ChildItem -Path (Get-PSKoanLocation) -Filter *.koans.ps1 -File -Recurse | + Select-Object -First 1 + } - $file.FullName | Should -Exist - } + It 'should copy missing topic files' { + $file | Remove-Item + $file.FullName | Should -Not -Exist - It 'should move incorrectly placed topics' { - $directory = New-Item -Path (Join-Path -Path $TestDrive -ChildPath 'PSKoans\Wrong') -ItemType Directory - $file | Move-Item -Destination $directory.FullName - $file.FullName | Should -Not -Exist + Update-PSKoan -Confirm:$false - Update-PSKoan -Confirm:$false + $file.FullName | Should -Exist + } - $file.FullName | Should -Exist - } + It 'should move incorrectly placed topics' { + $directory = New-Item -Path (Join-Path -Path $TestDrive -ChildPath 'PSKoans\Wrong') -ItemType Directory + $file | Move-Item -Destination $directory.FullName + $file.FullName | Should -Not -Exist - It 'should remove discarded topics' { - $oldTopicPath = Join-Path $TestDrive 'PSKoans\Foundations\OldTopic.koans.ps1' - $file | Copy-Item -Destination $oldTopicPath - $oldTopicPath | Should -Exist + Update-PSKoan -Confirm:$false - Update-PSKoan -Confirm:$false + $file.FullName | Should -Exist + } - $oldTopicPath | Should -Not -Exist - } + It 'should remove discarded topics' { + $oldTopicPath = Join-Path $TestDrive 'PSKoans\Foundations\OldTopic.koans.ps1' + $file | Copy-Item -Destination $oldTopicPath + $oldTopicPath | Should -Exist + + Update-PSKoan -Confirm:$false + + $oldTopicPath | Should -Not -Exist } } } diff --git a/Tests/KoanValidation.Tests.ps1 b/Tests/KoanValidation.Tests.ps1 index 76708e75b..ac6b8e6c3 100644 --- a/Tests/KoanValidation.Tests.ps1 +++ b/Tests/KoanValidation.Tests.ps1 @@ -3,33 +3,27 @@ using namespace System.Management.Automation.Language using namespace System.Collections.Generic -$ProjectRoot = Resolve-Path "$PSScriptRoot/.." -$KoanFolder = $ProjectRoot | - Join-Path -ChildPath 'PSKoans' -AdditionalChildPath 'Koans' - -Describe 'Koan Topics Static Analysis Checks' { +Describe 'Static Analysis: Koan Topics' { 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 - } + #region Discovery + $KoanTopics = Resolve-Path "$PSScriptRoot/../PSKoans/Koans" | + Get-ChildItem -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 'Koan Topic should be valid powershell' -TestCases $KoanTopics { - param($File) + } + #endregion Discovery + It 'has no syntax errors in ' -TestCases $KoanTopics { $File.FullName | Should -Exist $Errors = $Tokens = $null @@ -37,9 +31,7 @@ Describe 'Koan Topics Static Analysis Checks' { $Errors.Count | Should -Be 0 } - It 'Koan Topic should not have nested It blocks' -TestCases $KoanTopics { - param($File) - + It 'does not have nested It blocks in ' -TestCases $KoanTopics { function Test-ItBlock { [CmdletBinding()] param([Ast] $element) @@ -86,14 +78,12 @@ Describe 'Koan Topics Static Analysis Checks' { $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) - + It 'has exactly one line feed at end of the file' -TestCases $KoanTopics { $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 { + It 'has a position number defined for ' -TestCases $KoanTopics { param($File, $Position) $Position | Should -Not -BeNullOrEmpty @@ -103,9 +93,23 @@ Describe 'Koan Topics Static Analysis Checks' { Context 'Library Cleanliness' { - It 'should not have topics with duplicate Koan positions' { - $DuplicatePosition = $KoanTopics | - ForEach-Object { [PSCustomObject]$_ } | + BeforeAll { + $KoanFolder = Resolve-Path "$PSScriptRoot/../PSKoans/Koans" + } + + It 'does not have topics with duplicate Koan positions' { + $DuplicatePosition = 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' } + + [PSCustomObject]@{ + File = $_ + Name = $_.BaseName -replace '\.Koans$' + Position = $koanAttribute.Position + Module = $koanAttribute.Module + } + } | Group-Object { '{0}/{1}' -f $_.Module, $_.Position } | Where-Object Count -gt 1 | ForEach-Object { '{0}: {1}' -f $_.Name, ($_.Group.File -join ', ') } @@ -113,7 +117,7 @@ Describe 'Koan Topics Static Analysis Checks' { $DuplicatePosition | Should -BeNullOrEmpty } - It 'should not have non-Koan Topic files in the Koans directory' { + It 'does not have non-Koan Topic files in the Koans directory' { Get-ChildItem -Path $KoanFolder -Recurse -Filter '*.ps1' | Where-Object BaseName -notmatch '\.Koans$' | Should -BeNullOrEmpty diff --git a/Tests/ModuleValidation.Tests.ps1 b/Tests/ModuleValidation.Tests.ps1 index 3ee4a0d22..b13f7c992 100644 --- a/Tests/ModuleValidation.Tests.ps1 +++ b/Tests/ModuleValidation.Tests.ps1 @@ -1,49 +1,52 @@ -$ProjectRoot = Resolve-Path "$PSScriptRoot/.." -$ModuleRoot = Split-Path (Resolve-Path "$ProjectRoot/*/*.psm1") -$ModuleName = Split-Path $ModuleRoot -Leaf - -Describe "General project validation: $ModuleName" { - - BeforeAll { - $FileSearch = @{ - Path = $ProjectRoot - Include = '*.ps1', '*.psm1', '*.psd1' - Recurse = $true - Exclude = '*.Koans.ps1' - } - $Scripts = Get-ChildItem @FileSearch - - # TestCases are splatted to the script so we need hashtables - $TestCases = $Scripts | ForEach-Object { @{File = $_ } } +Describe 'Static Analysis: Module & Repository Files' { + + #region Discovery + $FileSearch = @{ + Path = Resolve-Path "$PSScriptRoot/.." + Include = '*.ps1', '*.psm1', '*.psd1' + Recurse = $true + Exclude = '*.Koans.ps1' } + $Scripts = Get-ChildItem @FileSearch - It ' should be valid powershell' -TestCases $TestCases { - param($File) + $TestCases = $Scripts | ForEach-Object { @{ File = $_ } } + #endregion Discovery - $File.FullName | Should -Exist + Context 'Repository Code' { - $FileContents = Get-Content -Path $File.FullName -ErrorAction Stop - $Errors = $null - [System.Management.Automation.PSParser]::Tokenize($FileContents, [ref]$Errors) > $null - $Errors.Count | Should -Be 0 - } + It 'has no invalid syntax errors in ' -TestCases $TestCases { + $File.FullName | Should -Exist - It ' should include one (and only one) line feed at end of file' -TestCases $TestCases { - param($File) - $crlf = [Regex]::Match(($File | Get-Content -Raw), '(\r?(?\n))+\Z') - $crlf.Groups['lf'].Captures.Count | Should -Be 1 - } + $FileContents = Get-Content -Path $File.FullName -ErrorAction Stop + $Errors = $null + [System.Management.Automation.PSParser]::Tokenize($FileContents, [ref]$Errors) > $null + $Errors.Count | Should -Be 0 + } - It 'can cleanly import the module' { - { Import-Module (Join-Path $ModuleRoot "$ModuleName.psm1") -Force } | Should -Not -Throw + It 'has exactly one line feed at EOF in ' -TestCases $TestCases { + $crlf = [Regex]::Match(($File | Get-Content -Raw), '(\r?(?\n))+\Z') + $crlf.Groups['lf'].Captures.Count | Should -Be 1 + } } - It 'can remove and re-import the module without errors' { - $Script = { - Remove-Module $ModuleName - Import-Module (Join-Path -Path $ModuleRoot -ChildPath "$ModuleName.psm1") + Context 'Module Import' { + + BeforeAll { + $ModuleName = 'PSKoans' + $ModuleRoot = (Get-Module -Name $ModuleName).ModuleBase + } + + It 'cleanly imports the module' { + { Import-Module (Join-Path $ModuleRoot "$ModuleName.psm1") -Force } | Should -Not -Throw } - $Script | Should -Not -Throw + It 'removes and re-imports the module without errors' { + $Script = { + Remove-Module $ModuleName + Import-Module (Join-Path -Path $ModuleRoot -ChildPath "$ModuleName.psm1") + } + + $Script | Should -Not -Throw + } } } diff --git a/formatting/PSKoans.Controls.format.ps1 b/formatting/PSKoans.Controls.format.ps1 index 557ed2746..00acd3f10 100644 --- a/formatting/PSKoans.Controls.format.ps1 +++ b/formatting/PSKoans.Controls.format.ps1 @@ -57,7 +57,7 @@ Write-FormatCustomView -AsControl -Name Prompt.Koan -Action { Write-FormatViewExpression -ScriptBlock { & (Get-Module -Name PSKoans) { $ReplacementPattern = '$1 {0} ' -f [char]0x258c - (Get-Random -InputObject $script:MeditationStrings) -replace '^|(\r?\n)', $ReplacementPattern + ($script:MeditationStrings | Get-Random) -replace '^|(\r?\n)', $ReplacementPattern } } -ForegroundColor 'PSKoans.Meditation.Emphasis' @@ -123,38 +123,87 @@ Write-FormatCustomView -AsControl -Name Prompt.Details -Action { Write-FormatViewExpression -Newline Write-FormatViewExpression -If { - $_.Describe -and - $_.Describe -ne $global:_Koan_Describe + $_.Block.Name -and + $_.Block.Name -ne $global:_Koan_Block_Name } -ScriptBlock { - $global:_Koan_Describe = $_.Describe - $Indent = " " * 4 - - '{0}Describing {1}{2}' -f $Indent, $_.Describe, [Environment]::NewLine - } -ForegroundColor 'PSKoans.Meditation.Text' + function Get-Depth { + param($Block) + + if (-not $Block) { + 0 + return + } + + if (-not $Block.Parent) { + 1 + } + else { + 1 + (Get-Depth -Block $Block.Parent) + } + } - Write-FormatViewExpression -If { - $_.Context -and - $_.Context -ne $global:_Koan_Context - } -ScriptBlock { - $global:_Koan_Context = $_.Context - $IndentSpaces = 4 - if ($_.Context) { $IndentSpaces += 2 } - $Indent = " " * $IndentSpaces + $global:_Koan_Block_Name = $_.Block.Name + $Depth = Get-Depth $_.Block + $Indent = " " * (4 * $Depth) - '{0}{1}{2}' -f $Indent, $_.Context, [Environment]::NewLine - } -ForegroundColor 'PSKoans.Meditation.Emphasis' + '{0}|{1}| {2}{3}' -f $Indent, [char]0x39e, $_.Block.Name, [Environment]::NewLine + } -ForegroundColor 'PSKoans.Meditation.Text' + <# OMITTED - Pester v5 doesn't distinguish its blocks after runs (Describe/Context aren't distinguishable) + Write-FormatViewExpression -If { + $_.Context -and + $_.Context -ne $global:_Koan_Context + } -ScriptBlock { + $global:_Koan_Context = $_.Context + $IndentSpaces = 4 + if ($_.Context) { $IndentSpaces += 2 } + $Indent = " " * $IndentSpaces + + '{0}{1}{2}' -f $Indent, $_.Context, [Environment]::NewLine + } -ForegroundColor 'PSKoans.Meditation.Emphasis' + #> Write-FormatViewExpression -If { $_.Passed } -ScriptBlock { - $IndentSpaces = 4 - if ($_.Context) { $IndentSpaces += 4 } elseif ($_.Describe) { $Indent += 2 } + function Get-Depth { + param($Block) + + if (-not $Block) { + 0 + return + } + + if (-not $Block.Parent) { + 1 + } + else { + 1 + (Get-Depth -Block $Block.Parent) + } + } + + $IndentSpaces = 2 + 4 * (Get-Depth $_.Block) $Indent = " " * $IndentSpaces '{0}[{1}] It {2}' -f $Indent, [char]0x25b8, $_.Name } -ForegroundColor 'PSKoans.Meditation.Passed' Write-FormatViewExpression -If { -not $_.Passed } -ScriptBlock { - $IndentSpaces = 4 - if ($_.Context) { $IndentSpaces += 4 } elseif ($_.Describe) { $Indent += 2 } + function Get-Depth { + param($Block) + + if (-not $Block) { + 0 + return + } + + if (-not $Block.Parent) { + 1 + } + else { + 1 + (Get-Depth -Block $Block.Parent) + } + } + + $Depth = Get-Depth $_.Block + $IndentSpaces = 2 + 4 * $Depth $Indent = " " * $IndentSpaces '{0}[{1}] It {2}' -f $Indent, [char]0xd7, $_.Name diff --git a/formatting/PSKoans.Result.format.ps1 b/formatting/PSKoans.Result.format.ps1 index 6010a09c4..066c1f710 100644 --- a/formatting/PSKoans.Result.format.ps1 +++ b/formatting/PSKoans.Result.format.ps1 @@ -80,8 +80,8 @@ Write-FormatView -TypeName PSKoans.Result -Name Detailed -Action { Write-FormatViewExpression -ScriptBlock { $_.Results } -ControlName Prompt.Details -Enumerate - $ExecutionContext.SessionState.PSVariable.Remove("Global:_Koan_Describe") - $ExecutionContext.SessionState.PSVariable.Remove("Global:_Koan_Context") + $ExecutionContext.SessionState.PSVariable.Remove("global:_Koan_Block_Name") + # $ExecutionContext.SessionState.PSVariable.Remove("global:_Koan_Context") Write-FormatViewExpression -If { $_.RequestedTopic.Count -ne 1 } -ControlName Prompt.ProgressBar -ScriptBlock { @{ diff --git a/templates/environment-setup.yml b/templates/environment-setup.yml index 8c47ba925..634a3f602 100644 --- a/templates/environment-setup.yml +++ b/templates/environment-setup.yml @@ -17,11 +17,11 @@ steps: $Params.Name = 'Pester' $Params.SkipPublisherCheck = $true - $Params.MaximumVersion = '4.99.99' + $Params.MinimumVersion = '5.0.2' $Params | Out-String | Write-Host Install-Module @Params $Params.Remove('SkipPublisherCheck') - $Params.Remove('MaximumVersion') + $Params.Remove('MinimumVersion') $Params.Name = 'EZOut' $Params.AllowClobber = $true diff --git a/templates/install-built-module.yml b/templates/install-built-module.yml index 9977e134f..268f4c651 100644 --- a/templates/install-built-module.yml +++ b/templates/install-built-module.yml @@ -28,7 +28,7 @@ steps: script: | $pesterParams = @{ Name = 'Pester' - MaximumVersion = '4.99.99' + MinimumVersion = '5.0.2' ProviderName = 'NuGet' Path = '${{ parameters.repositoryPath }}' Force = $true