├── LICENSE ├── PowerShellGuard.psd1 ├── PowerShellGuard.psm1 ├── README.md ├── appveyor.yml ├── build.ps1 └── test └── PowerShellGuard.Tests.ps1 /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Steven Murawski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /PowerShellGuard.psd1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smurawski/PowerShellGuard/af6f55a4d57827093c6e505b5718a9c7887fa9ac/PowerShellGuard.psd1 -------------------------------------------------------------------------------- /PowerShellGuard.psm1: -------------------------------------------------------------------------------- 1 | $script:Guards = @() 2 | 3 | function New-Guard 4 | { 5 | <# 6 | .SYNOPSIS 7 | Sets up a file system watcher to monitor files for changes, then run tests to verify things still work. 8 | .DESCRIPTION 9 | 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. 10 | .EXAMPLE 11 | New-Guard 12 | Watch the current directory for changes and run Pester on any changes. 13 | .EXAMPLE 14 | New-Guard -Path .\lib\chef\knife\ -PathFilter '*.rb' -Recurse -TestCommand rspec -TestPath .\spec\unit\knife\ 15 | Watch all .rb files under .\lib\chef\knife and when they change, run the unit tests 16 | .EXAMPLE 17 | dir *.ps1 | New-Guard -TestPath {"./Tests/$($_.basename).Tests.ps1"} 18 | Enumerate a directory and set up a test runner for each ps1 file based on its file name. For example hello.ps1 would have the test ./Tests/hello.Tests.ps1 19 | #> 20 | [cmdletbinding(SupportsShouldProcess=$true)] 21 | param ( 22 | # File or directory to monitor for changes 23 | [parameter( 24 | valuefrompipelinebypropertyname=$true)] 25 | $Path = $pwd, 26 | # Monitor recursively? 27 | [parameter( 28 | valuefrompipelinebypropertyname=$true)] 29 | [Alias('MonitorSubdirectories')] 30 | [switch] 31 | $Recurse, 32 | # Standard path filter syntax 33 | [parameter( 34 | valuefrompipelinebypropertyname=$true)] 35 | $PathFilter, 36 | # Command to execute to run tests. Defaults to Invoke-Pester. 37 | [parameter( 38 | valuefrompipelinebypropertyname=$true)] 39 | $TestCommand = 'Invoke-Pester', 40 | # File or directory containing the tests to run. 41 | [parameter(valuefrompipelinebypropertyname=$true)] 42 | [string] 43 | $TestPath, 44 | # Start monitoring running tests immediately. 45 | [parameter( 46 | valuefrompipelinebypropertyname=$true)] 47 | [switch] 48 | $Wait 49 | ) 50 | begin 51 | { 52 | if ($PSCmdlet.ShouldProcess('Creating the command queue')){ 53 | Set-GuardCommandQueue 54 | } 55 | } 56 | process 57 | { 58 | $Path = (resolve-path $Path).ProviderPath 59 | if (-not $psboundparameters.containskey('TestPath')) 60 | { 61 | $TestPath = $Path 62 | } 63 | else 64 | { 65 | $TestPath = (resolve-path $TestPath).ProviderPath 66 | } 67 | 68 | $GuardFileSystemWatcherActionParameters = @{ 69 | TestCommand = $TestCommand 70 | TestPath = $TestPath 71 | } 72 | 73 | if ($PSCmdlet.ShouldProcess("Creating file system watcher and registering the file path.")){ 74 | $FileSystemWatcherParameters = @{ 75 | Path = $Path 76 | Action = New-GuardFileSystemWatcherAction @GuardFileSystemWatcherActionParameters 77 | IncludeSubdirectories = $Recurse 78 | } 79 | if ($psboundparameters.containskey('pathfilter')) 80 | { 81 | $FileSystemWatcherParameters.PathFilter = $PathFilter 82 | } 83 | elseif (-not (get-item $path).PSIsContainer) 84 | { 85 | $FileSystemWatcherParameters.PathFilter = split-path -leaf $path 86 | $FileSystemWatcherParameters.Path = split-path $path 87 | } 88 | New-GuardFileSystemWatcher @FileSystemWatcherParameters 89 | } 90 | } 91 | end 92 | { 93 | if ($Wait) 94 | { 95 | Wait-Guard 96 | } 97 | } 98 | } 99 | 100 | function New-GuardFileSystemWatcherAction 101 | { 102 | [OutputType([System.Management.Automation.ScriptBlock])] 103 | [cmdletbinding(SupportsShouldProcess=$true)] 104 | param( $TestCommand, $TestPath) 105 | 106 | if ($PSCmdlet.ShouldProcess("Creating action scriptblock")){ 107 | $action = @" 108 | `$Parameters = @{ 109 | Path = `$eventargs.fullpath 110 | TestCommandString = '$TestCommand $TestPath' 111 | } 112 | 113 | Add-GuardQueueCommand @Parameters 114 | "@ 115 | [scriptblock]::create($action) 116 | } 117 | } 118 | 119 | function New-GuardFileSystemWatcher 120 | { 121 | [cmdletbinding(SupportsShouldProcess=$true)] 122 | param ( 123 | $path, 124 | $action, 125 | $PathFilter, 126 | [switch]$IncludeSubdirectories) 127 | 128 | if ($PSCmdlet.ShouldProcess($path, 'Creating FileSystemWatcher')){ 129 | $FileSystemWatcher = new-object IO.FileSystemWatcher $path 130 | } 131 | if ($PSCmdlet.ShouldProcess($IncludeSubdirectories, 'Including Subdirectories')){ 132 | $FileSystemWatcher.IncludeSubdirectories = $IncludeSubdirectories 133 | } 134 | if ($psboundparameters.containskey('PathFilter')) 135 | { 136 | if ($PSCmdlet.ShouldProcess($PathFilter, 'Path filter:')){ 137 | $FileSystemWatcher.Filter = $PathFilter 138 | } 139 | } 140 | Write-Verbose "`tUsing LastWrite as the notify filter." 141 | if ($PSCmdlet.ShouldProcess('Setting up LastWrite as the notify filter and registering the FileSystemWatcher.')) { 142 | $FileSystemWatcher.NotifyFilter = [IO.NotifyFilters]'LastWrite' 143 | $script:Guards += Register-ObjectEvent $FileSystemWatcher -EventName 'Changed' -Action $action 144 | } 145 | } 146 | 147 | function Add-GuardQueueCommand 148 | { 149 | param ( 150 | [string] 151 | $Path, 152 | [string] 153 | $TestCommandString 154 | ) 155 | if ($Path -notlike '*\.git*') 156 | { 157 | Get-GuardQueue 158 | $array = $script:GuardQueue.ToArray() 159 | if ($array -notcontains $TestCommandString) 160 | { 161 | Write-Verbose "$TestCommandString" 162 | $script:GuardQueue.enqueue("$TestCommandString") 163 | } 164 | } 165 | } 166 | 167 | 168 | 169 | function Wait-Guard 170 | { 171 | <# 172 | .SYNOPSIS 173 | Blocks and checks a queue for new tests to run. 174 | .DESCRIPTION 175 | Blocks and checks a queue for new tests to run. 176 | .EXAMPLE 177 | Wait-Guard -Seconds 10 178 | Starts blocking and checks the queue every 10 seconds. 179 | #> 180 | [cmdletbinding()] 181 | param( 182 | #Number of seconds to wait between queue checks 183 | [int] 184 | $Seconds = 5 185 | ) 186 | 187 | Get-GuardQueue 188 | do 189 | { 190 | if ($script:GuardQueue.count -gt 0) 191 | { 192 | clear-host 193 | $Command = $script:GuardQueue.dequeue() 194 | Write-Verbose $Command 195 | $ExecutionContext.InvokeCommand.InvokeScript($Command) 196 | } 197 | start-sleep -seconds $Seconds 198 | } while ($true) 199 | } 200 | 201 | function Remove-Guard 202 | { 203 | <# 204 | .SYNOPSIS 205 | Removes all the existing guards from a PowerShell session. 206 | .DESCRIPTION 207 | Removes all the existing guards from a PowerShell session. 208 | .EXAMPLE 209 | Remove-Guard 210 | #> 211 | [cmdletbinding()] 212 | param() 213 | foreach ($Guard in $script:Guards) 214 | { 215 | Get-EventSubscriber $Guard.Name | Unregister-Event 216 | $Guard | Remove-Job 217 | } 218 | Set-GuardCommandQueue -force 219 | $script:guards = @() 220 | } 221 | 222 | function Get-GuardQueue 223 | { 224 | $script:GuardQueue = [appdomain]::CurrentDomain.GetData('GuardQueue') 225 | } 226 | 227 | function Get-GuardQueuePeek 228 | { 229 | $script:GuardQueue.peek() 230 | } 231 | 232 | function Set-GuardCommandQueue 233 | { 234 | param ([switch] $force) 235 | if ((-not (Get-GuardQueue)) -or $force) 236 | { 237 | [appdomain]::CurrentDomain.SetData("GuardQueue", (new-object System.Collections.Queue)) 238 | } 239 | } 240 | 241 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build status](https://ci.appveyor.com/api/projects/status/e81t3vdaml1gxa9t/branch/master?svg=true)](https://ci.appveyor.com/project/smurawski/powershellguard/branch/master) 2 | 3 | # PowerShellGuard 4 | A [Guard](https://github.com/guard/guard) implementation for PowerShell, which allows for Pester tests to automatically be run whenever you make changes to a file. 5 | 6 | ## Why PowerShellGuard? 7 | 8 | ### Streamlined workflow. 9 | In conemu, I can dedicate a portion of my console window to just run tests. Using PowerShellGuard, I can watch my source code and trigger test runs of as much of my test harness as I want. This gives me fast feedback with minimal effort, making sure any changes that break existing behavior are noticed near when the changes are made. 10 | 11 | # Examples: 12 | 13 | ```powershell 14 | New-Guard -wait 15 | ``` 16 | Watch the files in the current directory for changes and run Pester on any changes. 17 | 18 | ```powershell 19 | New-Guard -Path .\lib\chef\knife\ -PathFilter '*.rb' -Recurse -TestCommand rspec -TestPath .\spec\unit\knife\ 20 | Wait-Guard 21 | ``` 22 | Watch all .rb files under .\lib\chef\knife and when they change, run the unit tests using RSpec. 23 | 24 | ```powershell 25 | dir *.ps1 | New-Guard -TestPath {"./Tests/$($_.basename).Tests.ps1"} -wait 26 | ``` 27 | Enumerate a directory and set up a test runner for each ps1 file based on its file name. For example hello.ps1 would have the test ./Tests/hello.Tests.ps1 28 | 29 | ## Installing PowerShellGuard 30 | 31 | You can install PowerShellGuard via PowerShellGet from the PowerShellGallery. 32 | 33 | ```powershell 34 | Install-Module PowerShellGuard 35 | ``` 36 | 37 | If you want the development feed (built from master), 38 | 39 | ```powershell 40 | Register-PSRepository -Name PowerShellGuard_current -SourceLocation 'https://ci.appveyor.com/nuget/PowerShellGuard/' 41 | ``` 42 | 43 | ```powershell 44 | Install-Module PowerShellGuard -Source PowerShellGuard_current 45 | ``` 46 | 47 | 48 | # Contributing 49 | 50 | * Source hosted at [GitHub][repo] 51 | * Report issues/questions/feature requests on [GitHub Issues][issues] 52 | 53 | Pull requests are very welcome! Make sure your patches are well tested. 54 | Ideally create a topic branch for every separate change you make. For 55 | example: 56 | 57 | 1. Fork the repo 58 | 2. Create your feature branch (`git checkout -b my-new-feature`) 59 | 3. Commit your changes (`git commit -am 'Added some feature'`) 60 | 4. Push to the branch (`git push origin my-new-feature`) 61 | 5. Create new Pull Request 62 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | #---------------------------------# 2 | # environment configuration # 3 | #---------------------------------# 4 | os: unstable 5 | install: 6 | - ps: 'Get-CimInstance win32_operatingsystem -Property Caption, OSArchitecture, Version | fl Caption, OSArchitecture, Version' 7 | - ps: $PSVersionTable 8 | - cinst -y pester 9 | 10 | #---------------------------------# 11 | # build configuration # 12 | #---------------------------------# 13 | branches: 14 | only: 15 | - master 16 | 17 | version: 0.8.{build} 18 | 19 | skip_tags: true 20 | 21 | pull_requests: 22 | do_not_increment_build_number: true 23 | 24 | nuget: 25 | disable_publish_on_pr: true 26 | 27 | build: off 28 | 29 | test_script: 30 | - ps: | 31 | $testResultsFile = ".\TestsResults.xml" 32 | $res = Invoke-Pester -OutputFormat NUnitXml -OutputFile $testResultsFile -PassThru 33 | (New-Object 'System.Net.WebClient').UploadFile("https://ci.appveyor.com/api/testresults/nunit/$($env:APPVEYOR_JOB_ID)", (Resolve-Path $testResultsFile)) 34 | if ($res.FailedCount -gt 0) { 35 | throw "$($res.FailedCount) tests failed." 36 | } 37 | 38 | deploy_script: 39 | - ps: | 40 | irm 'https://raw.githubusercontent.com/smurawski/AppVeyorSampleHelper/master/TestHelper.psm1' | invoke-expression 41 | 42 | $Manifest = Get-ModuleManifestHash 43 | Update-ModuleManifest $Manifest 44 | $UpdatedManifest = Get-ModuleManifestHash 45 | $nupkg = New-ModuleNugetPackage $UpdatedManifest 46 | $zip = New-ModuleZipFile 47 | Publish-BuildArtifact -Path $nupkg, $zip 48 | -------------------------------------------------------------------------------- /build.ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | [switch]$Syntax, 3 | [switch]$Unit, 4 | [switch]$Publish, 5 | [switch]$Version, 6 | [switch]$Promote, 7 | [string]$GalleryUri, 8 | [string]$NugetApiKey, 9 | [int]$Build 10 | ) 11 | function CreateModuleDir ($ModuleDir) { 12 | mkdir $ModuleDir -Force | Out-Null 13 | } 14 | 15 | if ($Syntax) { 16 | Install-Module -Name PSScriptAnalyzer -F -Scope CurrentUser 17 | Invoke-ScriptAnalyzer -Path . -Recurse | 18 | Where-Object severity -eq \"Warning\" | 19 | ForEach-Object { 20 | Write-Host "##vso[task.logissue type=$($_.Severity);sourcepath=$($_.ScriptPath);linenumber=$($_.Line);columnnumber=$($_.Column);]$($_.Message)" 21 | } 22 | } 23 | 24 | if ($Unit) { 25 | Invoke-Pester ./test -EnableExit -Strict -OutputFile test-results.xml -OutputFormat NUnitXml -passthru 26 | } 27 | 28 | if ($Version) { 29 | $ModuleDir = "$pwd/Release/PowerShellGuard" 30 | CreateModuleDir $ModuleDir 31 | get-content '.\PowerShellGuard.psd1' | 32 | ForEach-Object { $_ -replace '{BUILDNUMBER}', $Build} | 33 | set-content -Encoding UTF8 -Path $ModuleDir/PowerShellGuard.psd1 34 | } 35 | 36 | if ($Publish) { 37 | $ModuleDir = "$pwd/Release/PowerShellGuard" 38 | CreateModuleDir $ModuleDir 39 | Copy-Item './PowerShellGuard.psm1', './LICENSE', './README.md' -Destination $ModuleDir 40 | 41 | $PublishParameters = @{ 42 | Path = $ModuleDir 43 | NugetApiKey = $NugetApiKey 44 | Force = $true 45 | } 46 | if (-not [string]::IsNullOrEmpty($GalleryUri)) { 47 | Register-PSRepository -Name CustomFeed -SourceLocation $GalleryUri -PublishLocation "$($GalleryUri.trim('/'))/package" 48 | $PublishParameters.Repository = 'CustomFeed' 49 | } 50 | Install-PackageProvider -Name NuGet -Force -ForceBootstrap -scope CurrentUser 51 | Publish-Module @PublishParameters 52 | } 53 | 54 | if ($Promote) { 55 | $ModuleDir = "$pwd/Release/PowerShellGuard" 56 | CreateModuleDir $ModuleDir 57 | 58 | if (-not [string]::IsNullOrEmpty($GalleryUri)) { 59 | Register-PSRepository -Name CustomFeed -SourceLocation $GalleryUri -PublishLocation "$($GalleryUri.trim('/'))/package" 60 | Save-Module PowershellGuard -Repository CustomFeed -Path $pwd/Release 61 | } 62 | $PublishParameters = @{ 63 | Path = $ModuleDir 64 | NugetApiKey = $NugetApiKey 65 | Force = $true 66 | Repository = 'PSGallery' 67 | } 68 | 69 | Install-PackageProvider -Name NuGet -Force -ForceBootstrap -scope CurrentUser 70 | Publish-Module @PublishParameters 71 | } 72 | -------------------------------------------------------------------------------- /test/PowerShellGuard.Tests.ps1: -------------------------------------------------------------------------------- 1 | Import-Module $PSScriptRoot/../PowerShellGuard.psm1 -Force 2 | 3 | Describe 'PowerShellGuard' { 4 | InModuleScope 'PowerShellGuard' { 5 | context 'New-Guard' { 6 | mock Set-GuardCommandQueue -MockWith {} -Verifiable 7 | mock Resolve-Path -MockWith {[pscustomobject]@{ProviderPath = 'c:\test\test.ps1'}} -Verifiable 8 | mock Get-Item -ParameterFilter {$Path -like 'c:\test\test.ps1'} -MockWith {[pscustomobject]@{PSIsContainer=$false}} 9 | mock New-GuardFileSystemWatcherAction -MockWith {} -Verifiable 10 | mock New-GuardFileSystemWatcher -MockWith {} -Verifiable 11 | mock Wait-Guard -MockWith {} 12 | 13 | new-guard 14 | it 'calls Resolve-Path' { 15 | Assert-MockCalled -CommandName 'Resolve-Path' 16 | } 17 | it 'calls New-GuardFileSystemWatcherAction with default values' { 18 | Assert-MockCalled -CommandName New-GuardFileSystemWatcherAction -ParameterFilter {$TestCommand -eq 'Invoke-Pester' -and $TestPath -eq 'c:\test\test.ps1' } 19 | } 20 | it 'calls New-GuardFileSystemWatcherAction and sets up a file system watcher with the right path and filter' { 21 | Assert-MockCalled -CommandName 'New-GuardFileSystemWatcher' -ParameterFilter { 22 | $Path -eq 'c:\test' -and 23 | $PathFilter -eq 'test.ps1' -and 24 | $IncludeSubdirectories -eq $false 25 | } 26 | } 27 | } 28 | } 29 | } --------------------------------------------------------------------------------