ISEPester.psm1
#region prefix code #region content of file Classes enum Scope { ParentScope ChildScope } enum NotSaved { RunFromDisk RunFromTemp } enum Untitled { Ignore SaveAsTemp } class ISEPesterConfiguration { [Scope]$InvokeScope [NotSaved]$ActionNotSaved [Untitled]$ActionUntitled ISEPesterConfiguration () { $this.InvokeScope = [Scope]::ParentScope $this.ActionNotSaved = [NotSaved]::RunFromDisk $this.ActionUntitled = [Untitled]::Ignore } [ISEPesterConfiguration] Clone () { return [ISEPesterConfiguration]@{ InvokeScope = $this.InvokeScope ActionNotSaved = $this.ActionNotSaved ActionUntitled = $this.ActionUntitled } } } #endregion #region content of file ImportModule try { Import-Module -Name Pester -MinimumVersion 5.0 -ErrorAction Stop } catch { Write-Error -Message "Failed to import module Pester - can't continue. Error: $_" return } #endregion #region content of file InternalVariables $script:outputConfiguration = [Pester.OutputConfiguration]::Default $script:isePesterConfiguration = [ISEPesterConfiguration]::new() #endregion #endregion #region private functions #region content of file Get-psISE function Get-psISE { <# .Synopsis Helper function that returns psISE object (needed for testing /mocking) .Description $psISE automatic variable is read-only and can't be replaced inside ISE. To prevent not being able to test module inside that host - adding simple function that returns this object. .Example $test = Get-psISE Saves value of psISE object into variable test. #> [CmdletBinding()] param () $psISE } #endregion #endregion #region public functions #region content of file Invoke-ISECurrentTest function Invoke-ISECurrentTest { <# .Synopsis Function to run tests based on cursor location in editor file. .Description PowerShell ISE allows to see current location of the cursor in editor tab. Using that information makes it possible to run a test based on that location. Logic is: - if the cursor is on the line with container name (It, Context, Describe) that block is called. - if the cursor is on the line inside any `It` block - that block only would be called. .Example Invoke-ISECurrentTest Runs test on the line where cursor in the current file is located. #> [CmdletBinding()] param ( # Verbosity of output [ValidateSet( 'None', 'Normal', 'Detailed', 'Diagnostic' )] [String]$Verbosity, # Verbosity of stack trace [ValidateSet( 'None', 'FirstLine', 'Filtered', 'Full' )] [String]$StackTraceVerbosity, # The CI format of error output in build logs [ValidateSet( 'None', 'Auto', 'AzureDevops', 'GithubActions' )] [String]$CIFormat, # The scope where scripts should run [Scope]$InvokeScope, # Behavior for files that were not saved [NotSaved]$ActionNotSaved, # Behavior for filest that are untitled/ not saved to disk yet [Untitled]$ActionUntitled ) $currentIsePesterConfig = $script:isePesterConfiguration.clone() foreach ( $key in @( 'InvokeScope' 'ActionNotSaved' 'ActionUntitled' ) ) { if ($PSBoundParameters.ContainsKey($key)) { $currentIsePesterConfig.$key = $PSBoundParameters.$key } } try { $ise = Get-psISE } catch { Write-Warning -Message 'Command designed to use in PowerShell ISE' } if (-not (Get-Module -Name Pester)) { try { Import-Module -Name Pester -MinimumVersion 5.0 -ErrorAction Stop } catch { Write-Warning -Message "Failed to import Pester module - $_" } } $tempFile = $null if ( ($file = $ise.CurrentFile) -and ($line = $file.Editor.CaretLineText) -and ($lineNumber = $file.Editor.CaretLine) ) { if ($file.IsSaved) { $testFilePath = $file.FullPath } else { if ($file.IsUntitled) { if ($currentIsePesterConfig.ActionUntitled -eq [Untitled]::Ignore) { Write-Warning -Message "File not saved and ISEPester configure to ignore Untitled files - can't continue" return } else { $tempFile = [IO.Path]::GetTempFileName() | Get-Item | Rename-Item -NewName { '{0}.Tests.ps1' -f $_.Name } -PassThru Set-Content -Path $tempFile.FullName -Value $file.Editor.Text $testFilePath = $tempFile.FullName } } else { if ($currentIsePesterConfig.ActionNotSaved -eq [NotSaved]::RunFromDisk) { Write-Warning -Message "File $($file.FullPath) is not saved - working on current copy on disk!" $testFilePath = $file.FullPath } else { $tempFile = [IO.Path]::GetTempFileName() | Get-Item | Rename-Item -NewName { '{0}.Tests.ps1' -f $_.Name } -PassThru Set-Content -Path $tempFile.FullName -Value $file.Editor.Text $testFilePath = $tempFile.FullName } } } $config = [PesterConfiguration]@{ Run = @{ Path = $testFilePath } } foreach ( $parameter in @( 'Verbosity' 'StackTraceVerbosity' 'CIFormat' ) ) { if ($PSBoundParameters.ContainsKey($parameter)) { $config.Output.$parameter = $PSBoundParameters.$parameter } else { $config.Output.$parameter = $script:outputConfiguration.$parameter } } $parsedTestFile = [System.Management.Automation.Language.Parser]::ParseFile($testFilePath, [ref]$null, [ref]$null) $filter = '' if ($line -match '\s*(Describe|Context|It)') { # lets make sure this is not a comment... $myBlock = $parsedTestFile.FindAll( { param ( $Ast ) $Ast.CommandElements -and $Ast.CommandElements[0].Value -in 'It', 'Context', 'Describe' -and $Ast.Extent.StartLineNumber -eq $lineNumber }, $true ) if ($myBlock) { $filter = '{0}:{1}' -f $testFilePath, $lineNumber } } if ([String]::IsNullOrEmpty($filter)) { $myItBlock = $parsedTestFile.FindAll( { param ( $Ast ) $Ast.CommandElements -and $Ast.CommandElements[0].Value -eq 'It' -and $Ast.Extent.StartLineNumber -le $lineNumber -and $Ast.Extent.EndLineNumber -ge $lineNumber }, $true ) if ($myItBlock) { $filter = '{0}:{1}' -f $testFilePath, $myItBlock[0].Extent.StartLineNumber } else { Write-Warning -Message "Line '$line' at $lineNumber is not inside It block - perhaps $($file.FullPath) is not a test file?" return } } $config.Filter.Line = $filter if ($currentIsePesterConfig.InvokeScope -eq [Scope]::ParentScope) { Invoke-Pester -Configuration $config } else { & { Invoke-Pester -Configuration $config } } if ($tempFile) { $tempFile | Remove-Item -Force } } else { Write-Warning -Message 'Not able to figure out cursor position - giving up.' } } #endregion #region content of file Set-ISEPesterConfiguration function Set-ISEPesterConfiguration { <# .Synopsis Function to configure the way Pester tests run in the ISE. .Description Functions allows to configure how tests in context of ISE will behave. It includes: - output options - scoping (to prevent polluting current scope) .Example Set-ISEPesterConfiguration -Verbosity Detailed -StackTraceVerbosity Filtered Changes outpuf of pester calls to: - display detailed results - show filter view for stack trace .Example Set-ISEPesterConfiguration -Invoke ChildScope Configures command to run in the child scope to prevent polluting parent scope. #> [Diagnostics.CodeAnalysis.SuppressMessage( 'PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Changing configuration of the module, not the system state' )] [CmdletBinding()] param ( # Verbosity of output [ValidateSet( 'None', 'Normal', 'Detailed', 'Diagnostic' )] [String]$Verbosity, # Verbosity of stack trace [ValidateSet( 'None', 'FirstLine', 'Filtered', 'Full' )] [String]$StackTraceVerbosity, # The CI format of error output in build logs [ValidateSet( 'None', 'Auto', 'AzureDevops', 'GithubActions' )] [String]$CIFormat, # The scope where scripts should run [Scope]$InvokeScope, # Behavior for files that were not saved [NotSaved]$ActionNotSaved, # Behavior for filest that are untitled/ not saved to disk yet [Untitled]$ActionUntitled ) foreach ( $key in @( 'Verbosity' 'StackTraceVerbosity' 'CIFormat' ) ) { if ($PSBoundParameters.ContainsKey($key)) { $script:outputConfiguration.$key = $PSBoundParameters.$key } } foreach ( $key in @( 'InvokeScope' 'ActionNotSaved' 'ActionUntitled' ) ) { if ($PSBoundParameters.ContainsKey($key)) { $script:isePesterConfiguration.$key = $PSBoundParameters.$key } } } #endregion #endregion #region sufix #region content of file ISEAddOn try { $null = $psise.CurrentPowerShellTab.AddOnsMenu.Submenus.Add( 'Run in Pester', { Invoke-ISECurrentTest }, 'CTRL+F8' ) } catch { Write-Warning -Message "Failed to add shortcut - $_" } #endregion #endregion |