diff --git a/PSKoans/Init/InitializeCache.ps1 b/PSKoans/Init/InitializeCache.ps1 new file mode 100644 index 000000000..614517973 --- /dev/null +++ b/PSKoans/Init/InitializeCache.ps1 @@ -0,0 +1,9 @@ +if (-not (Test-Path -Path $Script:CachePath)) { + New-Item -Path $Script:CachePath -ItemType Directory > $null +} + +$Script:KoanResultCache = @{} + +foreach ($cacheItem in Get-ChildItem $Script:CachePath -Filter *.xml) { + $Script:KoanResultCache[$cacheItem.BaseName] = Import-Clixml -Path $cacheItem.FullName +} diff --git a/PSKoans/PSKoans.psm1 b/PSKoans/PSKoans.psm1 index a6358912b..4a72f63e2 100644 --- a/PSKoans/PSKoans.psm1 +++ b/PSKoans/PSKoans.psm1 @@ -15,6 +15,7 @@ $script:ModuleRoot = $PSScriptRoot $script:ConfigPath = '~/.config/PSKoans/config.json' +$script:CachePath = '~/.config/PSKoans/cache' $script:DefaultSettings = @{ KoanLocation = Resolve-Path -Path '~' | Join-Path -ChildPath 'PSKoans' Editor = 'code' diff --git a/PSKoans/Private/Add-KoanCachedResult.ps1 b/PSKoans/Private/Add-KoanCachedResult.ps1 new file mode 100644 index 000000000..992a2bf52 --- /dev/null +++ b/PSKoans/Private/Add-KoanCachedResult.ps1 @@ -0,0 +1,46 @@ +function Add-KoanCachedResult { + <# + .SYNOPSIS + Add an entry to the koan cache. + + .DESCRIPTION + Cache the results of running tests for this koan. This avoids the overhead of repeatedly running pester on unchanged content. + + .PARAMETER Path + The path to the koans test file. + + .PARAMETER PesterTests + The result of running tests against the file created by the Invoke-Koan command. + #> + + [CmdletBinding()] + param ( + [Parameter(Mandatory)] + [string] + $Path, + + [Parameter(Mandatory)] + [object] + $Result + ) + + $currentHash = (Get-FileHash -Path $Path).Hash + $cacheEntryName = [System.IO.Path]::GetFileNameWithoutExtension($Path) + + if ($Script:KoanResultCache.Contains($cacheEntryName) -and $currentHash -eq $Script:KoanResultCache[$cacheEntryName]['Hash']) { + # No work to do for this file. Return immediately. + return + } + + $cacheItemPath = Join-Path -Path $Script:CachePath -ChildPath ( + '{0}.xml' -f [System.IO.Path]::GetFileNameWithoutExtension($Path) + ) + + $cacheEntry = @{ + Hash = $currentHash + Result = $Result + } + $cacheEntry | Export-CliXml -Path $cacheItemPath -Depth 5 + + $Script:KoanResultCache[$cacheEntryName] = $cacheEntry +} diff --git a/PSKoans/Private/Get-KoanCachedResult.ps1 b/PSKoans/Private/Get-KoanCachedResult.ps1 new file mode 100644 index 000000000..c39575f09 --- /dev/null +++ b/PSKoans/Private/Get-KoanCachedResult.ps1 @@ -0,0 +1,30 @@ +function Get-KoanCachedResult { + <# + .SYNOPSIS + Add an entry to the koan cache. + + .DESCRIPTION + Cache the results of running tests for this koan. This avoids the overhead of repeatedly running pester on unchanged content. + + .PARAMETER Path + The path to the koans test file. + #> + + [CmdletBinding()] + param ( + [Parameter(Mandatory)] + [string] + $Path + ) + + $cacheEntryName = [System.IO.Path]::GetFileNameWithoutExtension($Path) + + if ($Script:KoanResultCache.ContainsKey($cacheEntryName)) { + $currentHash = (Get-FileHash -Path $Path).Hash + $cacheEntry = $Script:KoanResultCache[$cacheEntryName] + + if ($currentHash -eq $cacheEntry['Hash']) { + $cacheEntry['Result'] + } + } +} diff --git a/PSKoans/Private/Invoke-Koan.ps1 b/PSKoans/Private/Invoke-Koan.ps1 index f385ebbb9..66b836f9d 100644 --- a/PSKoans/Private/Invoke-Koan.ps1 +++ b/PSKoans/Private/Invoke-Koan.ps1 @@ -31,6 +31,11 @@ } } end { + $CachedResult = Get-KoanCachedResult -Path $ParameterSplat['Script'] + if ($CachedResult) { + return $CachedResult + } + try { $Requirements = [System.Management.Automation.Language.Parser]::ParseFile( $ParameterSplat.Script, @@ -73,6 +78,7 @@ $Status = $ps.BeginInvoke() + # Wait for the runspace in PS to support use of Control+C do { Start-Sleep -Milliseconds 100 } until ($Status.IsCompleted) $Result = $ps.EndInvoke($Status) @@ -82,6 +88,9 @@ foreach ($errorItem in $ps.Streams.Error) { $PSCmdlet.WriteError($errorItem) } + } else { + # Never cache when errors are raised during the pester run + Add-KoanCachedResult -Path $ParameterSplat['Script'] -Result $Result } $Result diff --git a/PSKoans/Private/Measure-Koan.ps1 b/PSKoans/Private/Measure-Koan.ps1 index 3fbc7169b..070c5004c 100644 --- a/PSKoans/Private/Measure-Koan.ps1 +++ b/PSKoans/Private/Measure-Koan.ps1 @@ -39,7 +39,7 @@ ) -join [System.IO.Path]::PathSeparator } process { - Write-Verbose "Discovering koans in [$($KoanInfo.Name -join '], [')]" + Write-Verbose "Discovering koans in [$($KoanInfo.Path -join '], [')]" $Result = & (Get-Module Pester) { [CmdletBinding()] diff --git a/PSKoans/Private/New-KoanRunspace.ps1 b/PSKoans/Private/New-KoanRunspace.ps1 index b39beda97..749889c35 100644 --- a/PSKoans/Private/New-KoanRunspace.ps1 +++ b/PSKoans/Private/New-KoanRunspace.ps1 @@ -26,12 +26,17 @@ function New-KoanRunspace { try { $script = { - param( $PSKoansPath ) + param( + $PesterPath, + $PSKoansPath + ) + Get-Module $PesterPath -ListAvailable | Import-Module Get-Module $PSKoansPath -ListAvailable | Import-Module } $ps.AddScript($script) > $null + $ps.AddParameter('PesterPath', (Get-Module Pester).ModuleBase) > $null $ps.AddParameter('PSKoansPath', $MyInvocation.MyCommand.Module.ModuleBase) > $null $ps.Invoke() > $null diff --git a/Start-DebugSession.ps1 b/Start-DebugSession.ps1 index f28582f67..07f50cf40 100644 --- a/Start-DebugSession.ps1 +++ b/Start-DebugSession.ps1 @@ -6,6 +6,10 @@ $env:PSModulePath = $( ) -join [System.IO.Path]::PathSeparator & $PSScriptRoot/PSKoans.ezformat.ps1 + +$pester = Get-Module pester -ListAvailable | Where-Object Version -eq '5.0.2' + +Import-Module (Join-Path -Path $pester.ModuleBase -ChildPath 'pester.psd1') Import-Module $PSScriptRoot/PSKoans -### Enter code to test below +### Enter code to test here diff --git a/Tests/Functions/Private/Add-KoanCachedResult.Tests.ps1 b/Tests/Functions/Private/Add-KoanCachedResult.Tests.ps1 new file mode 100644 index 000000000..866801a0c --- /dev/null +++ b/Tests/Functions/Private/Add-KoanCachedResult.Tests.ps1 @@ -0,0 +1,38 @@ +#Requires -Modules PSKoans + +Describe 'Add-KoanCachedResult' { + BeforeAll { + Mock 'Get-FileHash' { + [PSCustomObject]@{ + Hash = 'abcdef' + } + } + Mock 'Export-CliXml' + } + + It 'adds an entry to the cache when the entry does not exist' { + InModuleScope -ModuleName 'PSKoans' { + $Script:KoanResultCache = @{} + + Add-KoanCachedResult -Path 'AboutAssertions.Koans.ps1' -Result 'Result' + } + + Should -Invoke 'Get-FileHash' + Should -Invoke 'Export-CliXml' + } + + It 'does nothing when the cache entry already exists and hashes match' { + InModuleScope -ModuleName 'PSKoans' { + $Script:KoanResultCache = @{ + 'AboutAssertions.Koans' = @{ + Hash = 'abcdef' + } + } + + Add-KoanCachedResult -Path 'AboutAssertions.Koans.ps1' -Result 'Result' + } + + Should -Invoke 'Get-FileHash' + Should -Invoke 'Export-CliXml' -Times 0 + } +} diff --git a/Tests/Functions/Private/Get-KoanCachedResult.Tests.ps1 b/Tests/Functions/Private/Get-KoanCachedResult.Tests.ps1 new file mode 100644 index 000000000..66faee7b1 --- /dev/null +++ b/Tests/Functions/Private/Get-KoanCachedResult.Tests.ps1 @@ -0,0 +1,45 @@ +#Requires -Modules PSKoans + +Describe 'Get-KoanCachedResult' { + BeforeAll { + Mock 'Get-FileHash' { + [PSCustomObject]@{ + Hash = 'abcdef' + } + } + } + + It 'gets an entry when hashes match' { + InModuleScope -ModuleName 'PSKoans' { + $Script:KoanResultCache = @{ + 'AboutAssertions.Koans' = @{ + Hash = 'abcdef' + Result = 'Result' + } + } + + Get-KoanCachedResult -Path 'AboutAssertions.Koans.ps1' + } | Should -Be 'Result' + } + + It 'returns nothing when hashes do not match' { + InModuleScope -ModuleName 'PSKoans' { + $Script:KoanResultCache = @{ + 'AboutAssertions.Koans' = @{ + Hash = 'defabc' + Result = 'Result' + } + } + + Get-KoanCachedResult -Path 'AboutAssertions.Koans.ps1' + } | Should -BeNullOrEmpty + } + + It 'returns nothing when the item is not in the cache' { + InModuleScope -ModuleName 'PSKoans' { + $Script:KoanResultCache = @{} + + Get-KoanCachedResult -Path 'AboutAssertions.Koans.ps1' + } | Should -BeNullOrEmpty + } +} diff --git a/Tests/Functions/Private/Invoke-Koan.Tests.ps1 b/Tests/Functions/Private/Invoke-Koan.Tests.ps1 index 10e58523d..79c72abd7 100644 --- a/Tests/Functions/Private/Invoke-Koan.Tests.ps1 +++ b/Tests/Functions/Private/Invoke-Koan.Tests.ps1 @@ -3,6 +3,9 @@ Describe 'Invoke-Koan' { BeforeAll { + Mock Add-KoanCachedResult + Mock Get-KoanCachedResult + $testFile = @{ Script = "$PSScriptRoot/ControlTests/Invoke-Koan.Control_Tests.ps1" } } @@ -43,4 +46,34 @@ Describe 'Invoke-Koan' { ForEach-Object -MemberName GetType | Should -Be @([Exception], [NotImplementedException]) } + + Context 'item exists in cache' { + BeforeAll { + Mock Get-KoanCachedResult { + 'CachedResult' + } + } + + It 'returns a cached results if a cache entry exists' { + InModuleScope 'PSKoans' -Parameters $testFile { + param($Script) + Invoke-Koan @{ Script = $Script; PassThru = $true } + } | Should -Be 'CachedResult' + + Should -Invoke 'Get-KoanCachedResult' + Should -Invoke 'Add-KoanCachedResult' -Times 0 + } + } + + Context 'item does not exist in cache' { + It 'adds an item to the cache when the pester run is successful' { + InModuleScope 'PSKoans' -Parameters $testFile { + param($Script) + Invoke-Koan @{ Script = $Script; PassThru = $true } + } + + Should -Invoke 'Get-KoanCachedResult' + Should -Invoke 'Add-KoanCachedResult' + } + } }