PowerShellGuard.psm1
$script:Guards = @() function New-Guard { <# .SYNOPSIS Sets up a file system watcher to monitor files for changes, then run tests to verify things still work. .DESCRIPTION Sets up a file system watcher to monitor files for changes, then run tests to verify things still work. The default test running is Pester, but any test runner can be supplied. .EXAMPLE New-Guard Watch the current directory for changes and run Pester on any changes. .EXAMPLE New-Guard -Path .\lib\chef\knife\ -PathFilter '*.rb' -MonitorSubdirectories -TestCommand rspec -TestPath .\spec\unit\knife\ Watch all .rb files under .\lib\chef\knife and when they change, run the unit tests .EXAMPLE dir *.ps1 | New-Guard -TestPath {"./Tests/$($_.basename).Tests.ps1"} Enumerate a directory and set up a test runner for each ps1 file based on its file name. For example hello.p1 would have the test ./Tests/hello.Tests.ps1 #> [cmdletbinding()] param ( # File or directory to monitor for changes [parameter(valuefrompipelinebypropertyname=$true)] $Path = $pwd, # Monitor recursively? [switch] $MonitorSubdirectories, # Standard path filter syntax $PathFilter, # Command to execute to run tests. Defaults to Invoke-Pester. $TestCommand = 'Invoke-Pester', # File or directory containing the tests to run. [parameter(valuefrompipelinebypropertyname=$true)] $TestPath, # Start monitoring running tests immediately. [switch] $Wait ) begin { Set-GuardCommandQueue } process { $Path = (resolve-path $Path).path if (-not $psboundparameters.containskey('TestPath')) { $TestPath = $Path } else { $TestPath = (resolve-path $TestPath).Path } $GuardFileSystemWatcherActionParameters = @{ TestCommand = $TestCommand TestPath = $TestPath } $FileSystemWatcherParameters = @{ Path = $Path Action = New-GuardFileSystemWatcherAction @GuardFileSystemWatcherActionParameters IncludeSubdirectories = $MonitorSubdirectories } if ($psboundparameters.containskey('pathfilter')) { $FileSystemWatcherParameters.PathFilter = $PathFilter } elseif (-not (get-item $path).PSIsContainer) { $FileSystemWatcherParameters.PathFilter = split-path -leaf $path $FileSystemWatcherParameters.Path = split-path $path } New-GuardFileSystemWatcher @FileSystemWatcherParameters } end { if ($Wait) { Wait-Guard } } } function New-GuardFileSystemWatcherAction { [cmdletbinding()] param( $TestCommand, $TestPath) $action = @" `$Parameters = @{ Path = `$eventargs.fullpath TestCommandString = '$TestCommand $TestPath' } Add-GuardQueueCommand @Parameters "@ [scriptblock]::create($action) } function New-GuardFileSystemWatcher { [cmdletbinding()] param ( $path, $action, $PathFilter, [switch]$IncludeSubdirectories) $file = $null Write-Verbose "Creating file system watcher for $path" $FileSystemWatcher = new-object IO.FileSystemWatcher $path Write-Verbose "`tInclude subdirectories: $IncludeSubdirectories" $FileSystemWatcher.IncludeSubdirectories = $IncludeSubdirectories if ($psboundparameters.containskey('PathFilter')) { Write-Verbose "`tPath filter: $PathFilter" $FileSystemWatcher.Filter = $PathFilter } Write-Verbose "`tUsing LastWrite as the notify filter." $FileSystemWatcher.NotifyFilter = [IO.NotifyFilters]'LastWrite' $script:Guards += Register-ObjectEvent $FileSystemWatcher -EventName 'Changed' -Action $action } function Add-GuardQueueCommand { param ( [string] $Path, [string] $TestCommandString ) if ($Path -notlike '*\.git*') { Get-GuardQueue $array = $script:GuardQueue.ToArray() if ($array -notcontains $TestCommandString) { Write-Verbose "$TestCommandString" $script:GuardQueue.enqueue("$TestCommandString") } } } function Wait-Guard { <# .SYNOPSIS Blocks and checks a queue for new tests to run. .DESCRIPTION Blocks and checks a queue for new tests to run. .EXAMPLE Wait-Guard -Seconds 10 Starts blocking and checks the queue every 10 seconds. #> [cmdletbinding()] param( #Number of seconds to wait between queue checks [int] $Seconds = 5 ) Get-GuardQueue do { if ($script:GuardQueue.count -gt 0) { clear-host $Command = $script:GuardQueue.dequeue() Write-Verbose $Command invoke-expression "$Command" } start-sleep -seconds $Seconds } while ($true) } function Remove-Guard { <# .SYNOPSIS Removes all the existing guards from a PowerShell session. .DESCRIPTION Removes all the existing guards from a PowerShell session. .EXAMPLE Remove-Guard #> [cmdletbinding()] param() foreach ($Guard in $script:Guards) { Get-EventSubscriber $Guard.Name | Unregister-Event $Guard | Remove-Job } Set-GuardCommandQueue -force $script:guards = @() } function Get-GuardQueue { $script:GuardQueue = [appdomain]::CurrentDomain.GetData('GuardQueue') } function Get-GuardQueuePeek { $script:GuardQueue.peek() } function Set-GuardCommandQueue { param ([switch] $force) if ((-not (Get-GuardQueue)) -or $force) { [appdomain]::CurrentDomain.SetData("GuardQueue", (new-object System.Collections.Queue)) } } |