├── .vscode └── settings.json ├── 0.0.1.0 ├── Dockerfile ├── Functions │ ├── Get-TestList │ │ └── Get-Testlist.ps1 │ ├── Invoke-AST │ │ └── Invoke-AST.ps1 │ ├── New-DockerFile │ │ └── New-DockerFile.ps1 │ ├── New-DockerImage │ │ └── New-DockerImage.ps1 │ └── Start-Pestener │ │ └── Start-Pestener.ps1 ├── Pestener.psd1 ├── Pestener.psm1 └── Tests │ └── demo.tests.ps1 ├── Appveyor.yml ├── LICENCE ├── README.md └── pics ├── Pestener.png └── pester.png /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Placez vos paramètres dans ce fichier pour remplacer les paramètres par défaut et les paramètres utilisateur. 2 | { 3 | // Place your settings in this file to overwrite the default settings 4 | { 5 | "files.autoSave": "afterDelay", 6 | "window.zoomLevel": -2 7 | } 8 | } -------------------------------------------------------------------------------- /0.0.1.0/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM microsoft/nanoserver 2 | RUN mkdir C:\Pester 3 | RUN mkdir C:\workspace 4 | RUN powershell.exe -ExecutionPolicy Bypass -Command 'Install-Module Pester -Force' 5 | CMD powershell.exe -ExecutionPolicy Bypass -Command cd C:\Pester; Invoke-Pester -OutputFormat LegacyNUnitXml -OutputFile C:\Pester\NUnit.XML -EnableExit 6 | -------------------------------------------------------------------------------- /0.0.1.0/Functions/Get-TestList/Get-Testlist.ps1: -------------------------------------------------------------------------------- 1 | Function Get-TestList { 2 | [CmdletBinding()] 3 | <# 4 | .SYNOPSIS 5 | Return an objet containing every pester tests in the folder (and sub folders) 6 | 7 | .PARAMETER Path 8 | This parameter indicates the location of every Pester tests you want to be run. 9 | 10 | .PARAMETER Recurse 11 | If you want to go in depth :) 12 | 13 | .EXAMPLE 14 | Get-TestList -path D:\Pester\Tests -Recurse 15 | 16 | #> 17 | param ( 18 | # Insert some test about this directory 19 | [Parameter(Mandatory)] 20 | [String]$Path, 21 | [Switch]$Recurse 22 | ) 23 | BEGIN { 24 | # Do some cleaning of maybe existing previous variables 25 | Write-Verbose -Message "Cleaning variables" 26 | } 27 | PROCESS { 28 | Try { 29 | # Get all pester tests in the directory. 30 | Write-Verbose -Message "Gathering a list of every Pester tests stored in $($Path)" 31 | If ($Recurse) { 32 | Write-Verbose -Message "Recursive mode requested. Go in $($path) depth " 33 | $TestList = Get-ChildItem -LiteralPath $Path -Recurse -File -filter *.tests.ps1 -ErrorAction SilentlyContinue 34 | } 35 | else { 36 | $TestList = Get-ChildItem -LiteralPath $Path -File -filter *.tests.ps1 -ErrorAction SilentlyContinue 37 | } 38 | } 39 | Catch { 40 | # To be done: Do some better catching on possible exceptions. 41 | $TestList = $null 42 | Write-Error -Message "$($_.Exception.Message)" 43 | } 44 | 45 | } 46 | END { 47 | Write-Verbose -Message "Returning the Pester tests list requested" 48 | Return $TestList 49 | } 50 | 51 | } 52 | 53 | Export-ModuleMember Get-TestList -------------------------------------------------------------------------------- /0.0.1.0/Functions/Invoke-AST/Invoke-AST.ps1: -------------------------------------------------------------------------------- 1 | Function Get-ASTFromInput { 2 | [CmdletBinding()] 3 | [OutputType([System.Management.Automation.Language.ScriptBlockAst])] 4 | <# 5 | .SYNOPSIS 6 | This function will create an AST object from raw text. 7 | 8 | .PARAMETER Content 9 | The raw content you wanna parse to an AST object 10 | 11 | .EXAMPLE 12 | Get-ASTFromInput -Content $Content 13 | #> 14 | 15 | param( 16 | [Parameter(Mandatory)]$Content 17 | ) 18 | $ast = [System.Management.Automation.Language.Parser]::ParseInput($Content,[ref]$null,[ref]$Null) 19 | return $ast 20 | } 21 | 22 | Function Get-TestNameAndTestBlock { 23 | [OutputType([String])] 24 | <# 25 | .SYNOPSIS 26 | This function will parse every Describe blocks and InModulscope blocks into a collection storing name and value of each blocks. 27 | If InmoduleScope block contains describe blocks, this function, should avoid adding describe blocks another time when the parsing for inmodulscope is finish. 28 | 29 | .PARAMETER Content 30 | This parameter is the content of the pester sfript you want to parse. 31 | 32 | .EXAMPLE 33 | Get-TestNameAndTestBlock -Content (Get-Content D:\Path\To\Some\File.ps1 -raw) 34 | 35 | #> 36 | param( 37 | [Parameter(Mandatory)] 38 | [String]$Content 39 | ) 40 | 41 | $ast = Get-ASTFromInput -Content $Content 42 | $commandAST = $ast.FindAll({ $args[0] -is [System.Management.Automation.Language.CommandAst]}, $true) 43 | $output = @() 44 | $DescribesTreated = @() 45 | 46 | $ModuleScopeASTs = $commandAST | Where-Object { $PSItem.GetCommandName() -eq 'InModuleScope' } 47 | $describeASTs = $commandAST | Where-Object {$PSItem.GetCommandName() -eq 'Describe'} 48 | 49 | # InmoduleScope 50 | $i = 0 51 | if ($ModuleScopeASTS) { 52 | Foreach ($ModuleScope in $ModuleScopeASTs) { 53 | $InModuleScopeElement = $ModuleScope.CommandElements | Select-Object -First 2 | Where-Object {$PSitem.Value -ne 'InModuleScope'} 54 | 55 | # Check if Descibes stored in InModuleScope 56 | if ($ModuleScope.Extent.Text -match "Describe+.*") { 57 | 58 | $TempAST = Get-ASTFromInput -Content $($ModuleScope.Extent.Text) 59 | $CommandTempAST = $TempAST.FindAll({ $args[0] -is [System.Management.Automation.Language.CommandAst]}, $true) 60 | $TempdescribeASTs = $commandTempAST | Where-Object {$PSItem.GetCommandName() -eq 'Describe'} 61 | 62 | Switch -Exact ($TempdescribeASTs.CommandElements.StringConstantType) { 63 | 64 | 'DoubleQuoted' { 65 | $DescribesTreated += $($ExecutionContext.InvokeCommand.ExpandString($TempdescribeASTs.Value)) 66 | } 67 | 'SingleQuoted' { 68 | # if the test name is a single quoted string 69 | $DescribesTreated += $($TempdescribeASTs.CommandElements.value[1]) 70 | } 71 | } 72 | } 73 | 74 | # Change if InModuleScope double detected 75 | [Array]$Doubles = $output | Where-Object { $PSitem.Name -eq $($InModuleScopeElement.Value) } 76 | 77 | if ($doubles) { 78 | $Name = $($InModuleScopeElement.Value) + "$($i)" 79 | } 80 | else { 81 | $Name = $($InModuleScopeElement.Value) 82 | } 83 | 84 | Write-verbose "File Name: $Name" 85 | $output += New-Object -TypeName PSObject -Property @{ 86 | Name = $Name 87 | Content = $($ModuleScope.Extent.Text) } 88 | $i++ 89 | break 90 | } 91 | } 92 | 93 | # Describes 94 | if ($describeASTs) { 95 | foreach ($describeAST in $describeASTs) { 96 | 97 | $TestNameElement = $describeAST.CommandElements | Select-Object -First 2 | Where-Object -FilterScript {$PSitem.Value -ne 'Describe'} 98 | Switch -Exact ($TestNameElement.StringConstantType ) { 99 | 100 | 'DoubleQuoted' { 101 | # if the test name is a double quoted string 102 | if ($DescribesTreated -notcontains $($ExecutionContext.InvokeCommand.ExpandString($TestNameElement.Value))) { 103 | $output += New-Object -typename PSObject -property @{ 104 | #Add the test name as key and testBlock string as value 105 | 106 | Name = $($ExecutionContext.InvokeCommand.ExpandString($TestNameElement.Value)) 107 | Content = $($describeAST.Extent.Text) 108 | } 109 | } 110 | break 111 | } 112 | 'SingleQuoted' { 113 | # if the test name is a single quoted string 114 | if ($DescribesTreated -notcontains $($TestNameElement.Value)) { 115 | $output += New-Object -typename PSObject -property @{ 116 | Name = $($TestNameElement.Value) 117 | Content = $($describeAST.Extent.Text) 118 | } 119 | } 120 | break 121 | } 122 | default { 123 | throw 'TestName passed to Describe block should be a string' 124 | } 125 | 126 | } 127 | 128 | } # end foreach block 129 | } 130 | else { 131 | throw 'Describe block not found in the Test Body.' 132 | } 133 | 134 | Return $Output 135 | } 136 | 137 | 138 | Export-ModuleMember Get-TestNameAndTestBlock -------------------------------------------------------------------------------- /0.0.1.0/Functions/New-DockerFile/New-DockerFile.ps1: -------------------------------------------------------------------------------- 1 | Function New-DockerFile { 2 | [CmdletBinding()] 3 | <# 4 | .SYNOPSIS 5 | This function will help you create a new Dockerfile 6 | 7 | .PARAMETER Path 8 | This parameter indicates the location where yo want your dockerile to be generated 9 | 10 | .PARAMETER From 11 | Which docker image should be used at first to build your own one. 12 | 13 | .PARAMETER Maintener 14 | The fullname of the maintener of the image 15 | 16 | .PARAMETER MaintenerMail 17 | The maintenet mail adress 18 | 19 | .EXAMPLE 20 | New-DockerFile -Path $DockerFile -From $from -Maintener $Maintener -MaintenerMail $MaintenerMail 21 | 22 | #> 23 | param ( 24 | [Parameter(Mandatory)] 25 | [String]$Path, 26 | [String]$from = 'microsoft/nanoserver', 27 | [Parameter(Mandatory)] 28 | [String]$Maintener, 29 | [Parameter(Mandatory)] 30 | [String]$MaintenerMail 31 | 32 | ) 33 | 34 | BEGIN { 35 | 36 | Write-Verbose "Create some path for the script needs" 37 | $FullPath = Join-Path -Path $Path -ChildPath 'Dockerfile' -ErrorAction SilentlyContinue 38 | 39 | 40 | } 41 | PROCESS { 42 | 43 | Try { 44 | 45 | Write-Verbose -Message "Building the Pester command line." 46 | $PesterCMD = $PesterCMD + "-EnableExit -OutputFormat LegacyNUnitXml -OutputFile C:\Pester\NUnit.XML " 47 | 48 | Write-Verbose -Message "Starting creation of the Docker file in $($FullPath)" 49 | 50 | #Adding informations in the Dockerfile 51 | Write-Verbose -Message "Adding the 'FROM' information in $($FullPath)" 52 | Write-Output "FROM $($from)" | Out-File -FilePath $FullPath -Encoding utf8 -ErrorAction SilentlyContinue 53 | 54 | #Building the folder structure. 55 | Write-Verbose -Message "Building the folder structure" 56 | Write-Output "RUN mkdir C:\Pester" | Out-File -FilePath $FullPath -Encoding utf8 -ErrorAction SilentlyContinue -Append 57 | Write-Output "RUN mkdir C:\workspace" | Out-File -FilePath $FullPath -Encoding utf8 -ErrorAction SilentlyContinue -Append 58 | 59 | #Adding the Pester tests to be runned after the launch. 60 | Write-Verbose -Message "Adding the PowerShell command that will be launched at the container start" 61 | Write-Output "CMD powershell.exe -ExecutionPolicy Bypass -Command cd C:\Pester; Invoke-Pester $($PesterCMD)" | Out-File -FilePath $FullPath -Encoding utf8 -ErrorAction SilentlyContinue -Append 62 | 63 | 64 | } 65 | Catch { 66 | 67 | Write-Warning -Message "$($_.Exception.Message)" 68 | 69 | } 70 | 71 | } 72 | END { 73 | 74 | } 75 | 76 | 77 | } 78 | 79 | Export-ModuleMember New-DockerFile -------------------------------------------------------------------------------- /0.0.1.0/Functions/New-DockerImage/New-DockerImage.ps1: -------------------------------------------------------------------------------- 1 | Function New-DockerImage { 2 | [CmdletBinding()] 3 | <# 4 | .SYNOPSIS 5 | This function will trigger a docker build. 6 | 7 | .EXAMPLE 8 | New-DockerImage 9 | 10 | #> 11 | param () 12 | Try { 13 | Write-Verbose -Message "Starting the Docker image build process." 14 | docker build -t pestener . 15 | } 16 | Catch { 17 | Write-Error -Message "Impossible to build the image. Error: $($_.Exception.Message)" 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /0.0.1.0/Functions/Start-Pestener/Start-Pestener.ps1: -------------------------------------------------------------------------------- 1 | Function Start-Pestener { 2 | [CmdletBinding()] 3 | <# 4 | .SYNOPSIS 5 | Provides an enhanced utilization of the Pester framework. Thanks to the Docker 6 | technology. It's easier to scale as much as you want in order to reduce the time 7 | of your tests. This module will split your Pester tests file describe blocs in separated 8 | file. Each file will be run by Pester in his own container. Once it's done you choose 9 | if you want to have the NUnit Exported or Not and if it needs to stop the build. 10 | Like that you can use this tools with things like TeamCity, Jenkins or TravisCI. 11 | 12 | .PARAMETER PesterFile 13 | This parameter indicate the PesterFile to parse. 14 | 15 | .PARAMETER TestPath 16 | This parameter indicates the location of every Pester tests you want to be run. 17 | 18 | .PARAMETER NewImage 19 | Use this switch if you want to generate a new docker image. 20 | 21 | .PARAMETER DockerFile 22 | This is the path where you want to generate your dockerfile 23 | 24 | .PARAMETER Workspace 25 | This is the path where is stored your workspace, a common data path, if you want to share artifact with your contianers. 26 | It'll be mounted aas C:\Workspace in the container. 27 | 28 | .PARAMETER From 29 | Which docker image should be used at first to build your own one. 30 | 31 | .PARAMETER Maintener 32 | The fullname of the maintener of the image 33 | 34 | .PARAMETER MaintenerMail 35 | The maintenet mail adress 36 | 37 | .PARAMETER MinRunspaces 38 | 39 | .PARAMETER MaxRunspaces 40 | 41 | 42 | .EXAMPLE 43 | Start-Pestener -PesterFile D:\git\Pestener\Tests\demo.tests.ps1 -Workspace D:\Git\Pestener -TestPath D:\temp -DockerFile D:\Git\Pestener -Maintener "Fabien Dibot" -MaintenerMail "fdibot@pwrshell.net" 44 | 45 | #> 46 | 47 | param ( 48 | [Parameter(Mandatory)] 49 | [String]$PesterFile, 50 | [Parameter(Mandatory)] 51 | [String]$Workspace, 52 | [Parameter(Mandatory)] 53 | [String]$TestPath, 54 | [String]$DockerFile, 55 | [String]$from = 'microsoft/nanoserver', 56 | [String]$Maintener, 57 | [String]$MaintenerMail, 58 | [Switch]$NewImage, 59 | [int]$MinRunspaces = 1, 60 | [int]$MaxRunspaces = 5 61 | 62 | ) 63 | 64 | BEGIN { 65 | 66 | } 67 | PROCESS { 68 | $apartmentstate = "MTA" 69 | $pool = [RunspaceFactory]::CreateRunspacePool($MinRunspaces,$MaxRunspaces) 70 | $pool.ApartmentState = $apartmentstate 71 | $pool.CleanupInterval = (New-TimeSpan -Minutes 1) 72 | $pool.Open() 73 | 74 | $runspaces = [System.Collections.ArrayList]@() 75 | 76 | $scriptblock = { 77 | Param ( 78 | [string]$testmount, 79 | [string]$workspace, 80 | [String]$PesterPath = 'c:\Pester', 81 | [String]$WorkspacePath = 'C:\Workspace' 82 | ) 83 | 84 | try { 85 | #$cmdOutput = 86 | cmd /c "docker run -ti -v ${testmount}:${PesterPath} -v ${workspace}:${WorkspacePath} pestener" '2>&1' 87 | } 88 | catch { } 89 | } 90 | 91 | # Create file for each describe bloc :) 92 | Get-TestList -Path $PesterFile | ForEach-Object { 93 | Write-Verbose -Message "File theaten $($PSItem.FullName)" 94 | # Gather the list of Describe block for each file 95 | Get-TestNameAndTestBlock -Content (Get-Content $PSItem.FullName -raw) | ForEach-Object { 96 | 97 | # Get rid of specials characters 98 | if ($PSItem.Name -like '*$*') { 99 | $FolderName = $($PSItem.Name).replace('$', '') 100 | } 101 | else { 102 | $FolderName = $PSItem.Name 103 | } 104 | 105 | # Create new pester specific directory 106 | Write-Verbose -Message "Folder: $($FolderName.replace(' ',''))" 107 | New-Item -Path (Join-Path -path $TestPath -childpath $FolderName.replace(' ','')) -ItemType Directory -Force | Out-Null 108 | 109 | # Create new test file in the previous direcotry 110 | Write-Verbose -Message "File: $($PSItem.Name.replace(' ',''))" 111 | New-Item -Path (Join-Path -path (Join-Path -path $TestPath -childpath $($PSItem.Name.replace(' ',''))) -ChildPath "run.tests.ps1") -ItemType File -Value $($PSItem.content) -Force | Out-Null 112 | 113 | } 114 | 115 | } 116 | 117 | if ($NewImage) { 118 | # Verify that all needed arguments are here. 119 | if (!(($DockerFile) -and ($Maintener) -and ($MaintenerMail))) {} 120 | 121 | # Create the new docker file 122 | New-DockerFile -Path $DockerFile -From $from -Maintener $Maintener -MaintenerMail $MaintenerMail 123 | 124 | # Build the new image 125 | New-DockerImage 126 | } 127 | 128 | 129 | # Start a container for each Pester tests file 130 | 131 | $Tests = Get-ChildItem -LiteralPath $TestPath -Recurse -File 132 | 133 | $Tests | ForEach-Object { 134 | 135 | $DirectoryName = $($PSItem.FullName).split('\')[-2] 136 | $testmount = Join-Path -Path $testpath -ChildPath $DirectoryName 137 | $runspace = [PowerShell]::Create() 138 | $null = $runspace.AddScript($scriptblock) 139 | $null = $runspace.AddArgument($testmount) 140 | $null = $runspace.AddArgument($workspace) 141 | $runspace.RunspacePool = $pool 142 | [void]$runspaces.Add([PSCustomObject]@{ Pipe = $runspace; Status = $runspace.BeginInvoke() }) 143 | 144 | } 145 | 146 | # Wait for runspaces to complete 147 | while ($runspaces.Status.IsCompleted -notcontains $true) {} 148 | 149 | $pool.Close() 150 | $pool.Dispose() 151 | } 152 | END { 153 | 154 | } 155 | 156 | 157 | } 158 | 159 | Export-ModuleMember Start-Pestener -------------------------------------------------------------------------------- /0.0.1.0/Pestener.psd1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fabiendibot/Pestener/0e535dddf48a652afbdae465fded3338bddbc244/0.0.1.0/Pestener.psd1 -------------------------------------------------------------------------------- /0.0.1.0/Pestener.psm1: -------------------------------------------------------------------------------- 1 | 2 | $FunctionsPath = "$PSScriptRoot\Functions" 3 | Get-ChildItem $FunctionsPath -Directory -Exclude "Tests" | ForEach-Object { 4 | . (join-path $FunctionsPath (Join-Path $_.BaseName "$($PSItem.BaseName).ps1")) 5 | } -------------------------------------------------------------------------------- /0.0.1.0/Tests/demo.tests.ps1: -------------------------------------------------------------------------------- 1 | $here = Split-Path -Parent $MyInvocation.MyCommand.Path 2 | 3 | $manifestPath = "$here\Pester.psd1" 4 | $changeLogPath = "$here\CHANGELOG.md" 5 | 6 | # DO NOT CHANGE THIS TAG NAME; IT AFFECTS THE CI BUILD. 7 | 8 | Describe "Pester manifest and changelog" -Tags 'VersionChecks' { 9 | $script:manifest = $null 10 | It "has a valid manifest" { 11 | { 12 | $script:manifest = Test-ModuleManifest -Path $manifestPath -ErrorAction Stop -WarningAction SilentlyContinue 13 | } | Should Not Throw 14 | } 15 | 16 | It "has a valid name in the manifest" { 17 | $script:manifest.Name | Should Be Pester 18 | } 19 | 20 | It "has a valid guid in the manifest" { 21 | $script:manifest.Guid | Should Be 'a699dea5-2c73-4616-a270-1f7abb777e71' 22 | } 23 | 24 | It "has a valid version in the manifest" { 25 | $script:manifest.Version -as [Version] | Should Not BeNullOrEmpty 26 | } 27 | 28 | $script:changelogVersion = $null 29 | It "has a valid version in the changelog" { 30 | 31 | foreach ($line in (Get-Content $changeLogPath)) 32 | { 33 | if ($line -match "^\D*(?(\d+\.){1,3}\d+)") 34 | { 35 | $script:changelogVersion = $matches.Version 36 | break 37 | } 38 | } 39 | $script:changelogVersion | Should Not BeNullOrEmpty 40 | $script:changelogVersion -as [Version] | Should Not BeNullOrEmpty 41 | } 42 | 43 | It "changelog and manifest versions are the same" { 44 | $script:changelogVersion -as [Version] | Should be ( $script:manifest.Version -as [Version] ) 45 | } 46 | 47 | if (Get-Command git.exe -ErrorAction SilentlyContinue) 48 | { 49 | $skipVersionTest = -not [bool]((git remote -v 2>&1) -match "github.com/Pester/") 50 | $script:tagVersion = $null 51 | It "is tagged with a valid version" -skip:$skipVersionTest { 52 | $thisCommit = git.exe log --decorate --oneline HEAD~1..HEAD 53 | 54 | if ($thisCommit -match 'tag:\s*(\d+(?:\.\d+)*)') 55 | { 56 | $script:tagVersion = $matches[1] 57 | } 58 | 59 | $script:tagVersion | Should Not BeNullOrEmpty 60 | $script:tagVersion -as [Version] | Should Not BeNullOrEmpty 61 | } 62 | 63 | It "all versions are the same" -skip:$skipVersionTest { 64 | $script:changelogVersion -as [Version] | Should be ( $script:manifest.Version -as [Version] ) 65 | $script:manifest.Version -as [Version] | Should be ( $script:tagVersion -as [Version] ) 66 | } 67 | 68 | } 69 | } 70 | 71 | if ($PSVersionTable.PSVersion.Major -ge 3) 72 | { 73 | $error.Clear() 74 | Describe 'Clean treatment of the $error variable' { 75 | Context 'A Context' { 76 | It 'Performs a successful test' { 77 | $true | Should Be $true 78 | } 79 | } 80 | 81 | It 'Did not add anything to the $error variable' { 82 | $error.Count | Should Be 0 83 | } 84 | } 85 | 86 | InModuleScope Pester { 87 | Describe 'SafeCommands table' { 88 | $path = $ExecutionContext.SessionState.Module.ModuleBase 89 | $filesToCheck = Get-ChildItem -Path $path -Recurse -Include *.ps1,*.psm1 -Exclude *.Tests.ps1 90 | $callsToSafeCommands = @( 91 | foreach ($file in $files) 92 | { 93 | $tokens = $parseErrors = $null 94 | $ast = [System.Management.Automation.Language.Parser]::ParseFile($file.FullName, [ref] $tokens, [ref] $parseErrors) 95 | $filter = { 96 | $args[0] -is [System.Management.Automation.Language.CommandAst] -and 97 | $args[0].InvocationOperator -eq [System.Management.Automation.Language.TokenKind]::Ampersand -and 98 | $args[0].CommandElements[0] -is [System.Management.Automation.Language.IndexExpressionAst] -and 99 | $args[0].CommandElements[0].Target -is [System.Management.Automation.Language.VariableExpressionAst] -and 100 | $args[0].CommandElements[0].Target.VariablePath.UserPath -match '^(?:script:)?SafeCommands$' 101 | } 102 | 103 | $ast.FindAll($filter, $true) 104 | } 105 | ) 106 | 107 | $uniqueSafeCommands = $callsToSafeCommands | ForEach-Object { $_.CommandElements[0].Index.Value } | Select-Object -Unique 108 | 109 | $missingSafeCommands = $uniqueSafeCommands | Where-Object { -not $script:SafeCommands.ContainsKey($_) } 110 | 111 | It 'The SafeCommands table contains all commands that are called from the module' { 112 | $missingSafeCommands | Should Be $null 113 | } 114 | } 115 | } 116 | } 117 | 118 | Describe 'Style rules' { 119 | $pesterRoot = (Get-Module Pester).ModuleBase 120 | 121 | $files = @( 122 | Get-ChildItem $pesterRoot -Include *.ps1,*.psm1 123 | Get-ChildItem $pesterRoot\Functions -Include *.ps1,*.psm1 -Recurse 124 | ) 125 | 126 | It 'Pester source files contain no trailing whitespace' { 127 | $badLines = @( 128 | foreach ($file in $files) 129 | { 130 | $lines = [System.IO.File]::ReadAllLines($file.FullName) 131 | $lineCount = $lines.Count 132 | 133 | for ($i = 0; $i -lt $lineCount; $i++) 134 | { 135 | if ($lines[$i] -match '\s+$') 136 | { 137 | 'File: {0}, Line: {1}' -f $file.FullName, ($i + 1) 138 | } 139 | } 140 | } 141 | ) 142 | 143 | if ($badLines.Count -gt 0) 144 | { 145 | throw "The following $($badLines.Count) lines contain trailing whitespace: `r`n`r`n$($badLines -join "`r`n")" 146 | } 147 | } 148 | 149 | It 'Pester Source Files all end with a newline' { 150 | $badFiles = @( 151 | foreach ($file in $files) 152 | { 153 | $string = [System.IO.File]::ReadAllText($file.FullName) 154 | if ($string.Length -gt 0 -and $string[-1] -ne "`n") 155 | { 156 | $file.FullName 157 | } 158 | } 159 | ) 160 | 161 | if ($badFiles.Count -gt 0) 162 | { 163 | throw "The following files do not end with a newline: `r`n`r`n$($badFiles -join "`r`n")" 164 | } 165 | } 166 | } 167 | 168 | InModuleScope Pester { 169 | Describe 'ResolveTestScripts' { 170 | Setup -File SomeFile.ps1 171 | Setup -File SomeFile.Tests.ps1 172 | Setup -File SomeOtherFile.ps1 173 | Setup -File SomeOtherFile.Tests.ps1 174 | 175 | It 'Resolves non-wildcarded file paths regardless of whether the file ends with Tests.ps1' { 176 | $result = @(ResolveTestScripts $TestDrive\SomeOtherFile.ps1) 177 | $result.Count | Should Be 1 178 | $result[0].Path | Should Be "$TestDrive\SomeOtherFile.ps1" 179 | } 180 | 181 | It 'Finds only *.Tests.ps1 files when the path contains wildcards' { 182 | $result = @(ResolveTestScripts $TestDrive\*.ps1) 183 | $result.Count | Should Be 2 184 | 185 | $paths = $result | Select-Object -ExpandProperty Path 186 | 187 | ($paths -contains "$TestDrive\SomeFile.Tests.ps1") | Should Be $true 188 | ($paths -contains "$TestDrive\SomeOtherFile.Tests.ps1") | Should Be $true 189 | } 190 | 191 | It 'Finds only *.Tests.ps1 files when the path refers to a directory and does not contain wildcards' { 192 | $result = @(ResolveTestScripts $TestDrive) 193 | 194 | $result.Count | Should Be 2 195 | 196 | $paths = $result | Select-Object -ExpandProperty Path 197 | 198 | ($paths -contains "$TestDrive\SomeFile.Tests.ps1") | Should Be $true 199 | ($paths -contains "$TestDrive\SomeOtherFile.Tests.ps1") | Should Be $true 200 | } 201 | 202 | It 'Assigns empty array and hashtable to the Arguments and Parameters properties when none are specified by the caller' { 203 | $result = @(ResolveTestScripts "$TestDrive\SomeFile.ps1") 204 | 205 | $result.Count | Should Be 1 206 | $result[0].Path | Should Be "$TestDrive\SomeFile.ps1" 207 | 208 | ,$result[0].Arguments | Should Not Be $null 209 | ,$result[0].Parameters | Should Not Be $null 210 | 211 | $result[0].Arguments.GetType() | Should Be ([object[]]) 212 | $result[0].Arguments.Count | Should Be 0 213 | 214 | $result[0].Parameters.GetType() | Should Be ([hashtable]) 215 | $result[0].Parameters.PSBase.Count | Should Be 0 216 | } 217 | 218 | Context 'Passing in Dictionaries instead of Strings' { 219 | It 'Allows the use of a "P" key instead of "Path"' { 220 | $result = @(ResolveTestScripts @{ P = "$TestDrive\SomeFile.ps1" }) 221 | 222 | $result.Count | Should Be 1 223 | $result[0].Path | Should Be "$TestDrive\SomeFile.ps1" 224 | } 225 | 226 | $testArgs = @('I am a string') 227 | It 'Allows the use of an "Arguments" key in the dictionary' { 228 | $result = @(ResolveTestScripts @{ Path = "$TestDrive\SomeFile.ps1"; Arguments = $testArgs }) 229 | 230 | $result.Count | Should Be 1 231 | $result[0].Path | Should Be "$TestDrive\SomeFile.ps1" 232 | 233 | $result[0].Arguments.Count | Should Be 1 234 | $result[0].Arguments[0] | Should Be 'I am a string' 235 | } 236 | 237 | It 'Allows the use of an "Args" key in the dictionary' { 238 | $result = @(ResolveTestScripts @{ Path = "$TestDrive\SomeFile.ps1"; Args = $testArgs }) 239 | 240 | $result.Count | Should Be 1 241 | $result[0].Path | Should Be "$TestDrive\SomeFile.ps1" 242 | 243 | $result[0].Arguments.Count | Should Be 1 244 | $result[0].Arguments[0] | Should Be 'I am a string' 245 | } 246 | 247 | It 'Allows the use of an "A" key in the dictionary' { 248 | $result = @(ResolveTestScripts @{ Path = "$TestDrive\SomeFile.ps1"; A = $testArgs }) 249 | 250 | $result.Count | Should Be 1 251 | $result[0].Path | Should Be "$TestDrive\SomeFile.ps1" 252 | 253 | $result[0].Arguments.Count | Should Be 1 254 | $result[0].Arguments[0] | Should Be 'I am a string' 255 | } 256 | 257 | $testParams = @{ MyKey = 'MyValue' } 258 | It 'Allows the use of a "Parameters" key in the dictionary' { 259 | $result = @(ResolveTestScripts @{ Path = "$TestDrive\SomeFile.ps1"; Parameters = $testParams }) 260 | 261 | $result.Count | Should Be 1 262 | $result[0].Path | Should Be "$TestDrive\SomeFile.ps1" 263 | 264 | $result[0].Parameters.PSBase.Count | Should Be 1 265 | $result[0].Parameters['MyKey'] | Should Be 'MyValue' 266 | } 267 | 268 | It 'Allows the use of a "Params" key in the dictionary' { 269 | $result = @(ResolveTestScripts @{ Path = "$TestDrive\SomeFile.ps1"; Params = $testParams }) 270 | 271 | $result.Count | Should Be 1 272 | $result[0].Path | Should Be "$TestDrive\SomeFile.ps1" 273 | 274 | $result[0].Parameters.PSBase.Count | Should Be 1 275 | $result[0].Parameters['MyKey'] | Should Be 'MyValue' 276 | } 277 | 278 | It 'Throws an error if no Path is specified' { 279 | { ResolveTestScripts @{} } | Should Throw 280 | } 281 | 282 | It 'Throws an error if a Parameters key is used, but does not contain an IDictionary object' { 283 | { ResolveTestScripts @{ P='P'; Params = 'A string' } } | Should Throw 284 | } 285 | } 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /Appveyor.yml: -------------------------------------------------------------------------------- 1 | # See http://www.appveyor.com/docs/appveyor-yml for many more options 2 | 3 | # Skip on updates to the readme. 4 | # We can force this by adding [skip ci] or [ci skip] anywhere in commit message 5 | skip_commits: 6 | message: /updated readme.*/ 7 | 8 | 9 | install: 10 | - ps: 11 | Install-Module Pester 12 | - git clone https://github.com/fabiendibot/Pestener "C:\Program Files\WindowsPowerShell\Modules\Pestener" 13 | # Install Docker 14 | - ps: 15 | Install-Module -Name DockerMsftProvider -Repository PSGallery -Force -verbose 16 | Install-Package -Name docker -ProviderName DockerMsftProvider -Verbose 17 | 18 | build: false 19 | 20 | test_script: 21 | - ps: 22 | Import-Module Pestener -verbose 23 | Start-Pestener -TestPath .\tests -OutPutXML -Workspace C:\Jenkins -CleanWorkspace -DockerFilePath Dockerfile -Maintener 'Fabien Dibot' -MaintenerMail fdibot@pwrshell.net 24 | Invoke-Pester .\tests\run.tests.ps1 25 | 26 | deploy_script: 27 | - ps: Get-Content .\Artifacts.txt | Foreach-Object { Push-AppveyorArtifact $_ } 28 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Fabien Dibot 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Pestener](https://github.com/fabiendibot/Pestener/blob/master/pics/Pestener.png) 2 | 3 | # Pestener 4 | 5 | [![Documentation Status](https://readthedocs.org/projects/pestener/badge/?version=latest)](http://pestener.readthedocs.io/en/latest/?badge=latest) 6 | 7 | This version is beta, please do not use it in production right now :) 8 | 9 | ## What is Pestener ? 10 | 11 | Pestener is a new way to factorize the length of your [Pester](https://github.com/Pester) tests. 12 | Indeed, when you have so many [Pester](https://github.com/Pester) tests, it takes quite time for your build to finish. 13 | This module is here to help you reduce this time, thanks to Docker. 14 | 15 | It use the [Pester](https://github.com/Pester) file architecture to build some new Pester test file for each describe bloc. 16 | Right now, there are limitations: 17 | 18 | 1. You must use simple Describe or inmodulescope blocks in order to have the module Workspace. 19 | 2. If you want to launch a script before tests, run it in a describe block. 20 | 21 | ## Need some examples ? 22 | ```Powershell 23 | Import-Module Pestener.psm1 24 | Start-Pestener -PesterFile .\Tests\DSC.tests.ps1 -Workspace D:\Git\Pestener ` 25 | -testPath D:\temp -DockerFile D:\Git\Pestener -Maintener "Fabien Dibot" -MaintenerMail "fdibot@pwrshell.net" -NewImage 26 | ``` 27 | 28 | ## Notes 29 | Creator: 30 | * [Fabien Dibot](https://pwrshell.net) 31 | * [Twitter](https://twitter.com/fdibot) 32 | 33 | Licence Model: MIT See licence file [here](https://github.com/fabiendibot/Pestener/LICENCE) 34 | 35 | ## Links 36 | [Pester](https://github.com/Pester) 37 | [Docker](https://github.com/Docker) 38 | -------------------------------------------------------------------------------- /pics/Pestener.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fabiendibot/Pestener/0e535dddf48a652afbdae465fded3338bddbc244/pics/Pestener.png -------------------------------------------------------------------------------- /pics/pester.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fabiendibot/Pestener/0e535dddf48a652afbdae465fded3338bddbc244/pics/pester.png --------------------------------------------------------------------------------