Functions/Output.ps1
$script:ReportStrings = DATA { @{ StartMessage = "Executing all tests in '{0}'" FilterMessage = ' matching test name {0}' TagMessage = ' with Tags {0}' MessageOfs = "', '" CoverageTitle = 'Code coverage report:' CoverageMessage = 'Covered {2:P2} of {3:N0} analyzed {0} in {4:N0} {1}.' MissedSingular = 'Missed command:' MissedPlural = 'Missed commands:' CommandSingular = 'Command' CommandPlural = 'Commands' FileSingular = 'File' FilePlural = 'Files' Describe = 'Describing {0}' Script = 'Executing script {0}' Context = 'Context {0}' Margin = ' ' Timing = 'Tests completed in {0}' # If this is set to an empty string, the count won't be printed ContextsPassed = '' ContextsFailed = '' TestsPassed = 'Tests Passed: {0}, ' TestsFailed = 'Failed: {0}, ' TestsSkipped = 'Skipped: {0} ' TestsPending = 'Pending: {0}, ' TestsInconclusive = 'Inconclusive: {0}, ' TestsNotRun = 'NotRun: {0}' } } $script:ReportTheme = DATA { @{ Describe = 'Green' DescribeDetail = 'DarkYellow' Context = 'Cyan' ContextDetail = 'DarkCyan' Pass = 'DarkGreen' PassTime = 'DarkGray' Fail = 'Red' FailTime = 'DarkGray' Skipped = 'Yellow' SkippedTime = 'DarkGray' Pending = 'Gray' PendingTime = 'DarkGray' NotRun = 'Gray' NotRunTime = 'DarkGray' Total = 'Gray' Inconclusive = 'Gray' InconclusiveTime = 'DarkGray' Incomplete = 'Yellow' IncompleteTime = 'DarkGray' Foreground = 'White' Information = 'DarkGray' Coverage = 'White' CoverageWarn = 'DarkRed' } } function Format-PesterPath ($Path, [String]$Delimiter) { # -is check is not enough for the arrays, the incoming value will likely be object[] # so we have to check if we can upcast to our required type if ($null -eq $Path) { $null } elseif ($Path -is [String]) { $Path } elseif ($Path -is [hashtable]) { # a well formed pester hashtable contains Path $Path.Path } elseif ($null -ne ($path -as [hashtable[]])) { ($path | ForEach-Object { $_.Path }) -join $Delimiter } # needs to stay at the bottom because almost everything can be upcast to array of string elseif ($Path -as [String[]]) { $Path -join $Delimiter } } function Write-PesterStart { param( [Parameter(mandatory = $true, valueFromPipeline = $true)] $Context ) process { # if (-not ( $Context.Show | Has-Flag 'All, Fails, Header')) { # return # } $OFS = $ReportStrings.MessageOfs $hash = @{ Files = [System.Collections.ArrayList]@() ScriptBlocks = 0 } foreach ($c in $Context.Containers) { switch ($c.Type) { "File" { $null = $hash.Files.Add($c.Content.FullName) } "ScriptBlock" { $null = $hash.ScriptBlocks++ } Default { throw "$($c.Type) is not supported." } } } $message = $ReportStrings.StartMessage -f (Format-PesterPath $hash.Files -Delimiter $OFS) $message = "$message$(if (0 -lt $hash.ScriptBlocks) { ", and in $($hash.ScriptBlocks) scriptblocks." })" # todo write out filters that are applied # if ($PesterState.TestNameFilter) { # $message += $ReportStrings.FilterMessage -f "$($PesterState.TestNameFilter)" # } # if ($PesterState.ScriptBlockFilter) { # $m = $(foreach ($m in $PesterState.ScriptBlockFilter) { "$($m.Path):$($m.Line)" }) -join ", " # $message += $ReportStrings.FilterMessage -f $m # } # if ($PesterState.TagFilter) { # $message += $ReportStrings.TagMessage -f "$($PesterState.TagFilter)" # } & $SafeCommands['Write-Host'] $message -Foreground $ReportTheme.Foreground } } function ConvertTo-PesterResult { param( [String] $Name, [Nullable[TimeSpan]] $Time, [System.Management.Automation.ErrorRecord] $ErrorRecord ) $testResult = @{ Name = $Name Time = $time FailureMessage = "" StackTrace = "" ErrorRecord = $null Success = $false Result = "Failed" } if (-not $ErrorRecord) { $testResult.Result = "Passed" $testResult.Success = $true return $testResult } if (@('PesterAssertionFailed', 'PesterTestSkipped', 'PesterTestInconclusive', 'PesterTestPending') -contains $ErrorRecord.FullyQualifiedErrorID) { # we use TargetObject to pass structured information about the error. $details = $ErrorRecord.TargetObject $failureMessage = $details.Message $file = $details.File $line = $details.Line $Text = $details.LineText if (-not $Pester.Strict) { switch ($ErrorRecord.FullyQualifiedErrorID) { PesterTestInconclusive { $testResult.Result = "Inconclusive"; break; } PesterTestPending { $testResult.Result = "Pending"; break; } PesterTestSkipped { $testResult.Result = "Skipped"; break; } } } } else { $failureMessage = $ErrorRecord.ToString() $file = $ErrorRecord.InvocationInfo.ScriptName $line = $ErrorRecord.InvocationInfo.ScriptLineNumber $Text = $ErrorRecord.InvocationInfo.Line } $testResult.FailureMessage = $failureMessage $testResult.StackTrace = "at <ScriptBlock>, ${file}: line ${line}$([System.Environment]::NewLine)${line}: ${Text}" $testResult.ErrorRecord = $ErrorRecord return $testResult } function Remove-Comments ($Text) { $text -replace "(?s)(<#.*#>)" -replace "\#.*" } function Write-PesterReport { param ( [Parameter(mandatory = $true, valueFromPipeline = $true)] $RunResult ) # if(-not ($PesterState.Show | Has-Flag Summary)) { return } & $SafeCommands['Write-Host'] ($ReportStrings.Timing -f (Get-HumanTime ($RunResult.Duration + $RunResult.FrameworkDuration + $RunResult.DiscoveryDuration))) -Foreground $ReportTheme.Foreground $Success, $Failure = if ($RunResult.FailedCount -gt 0) { $ReportTheme.Foreground, $ReportTheme.Fail } else { $ReportTheme.Pass, $ReportTheme.Information } $Skipped = if ($RunResult.SkippedCount -gt 0) { $ReportTheme.Skipped } else { $ReportTheme.Information } $NotRun = if ($RunResult.NotRunCount -gt 0) { $ReportTheme.NotRun } else { $ReportTheme.Information } $Total = if ($RunResult.TestsCount -gt 0) { $ReportTheme.Total } else { $ReportTheme.Information } # $Pending = if ($RunResult.PendingCount -gt 0) { # $ReportTheme.Pending # } # else { # $ReportTheme.Information # } # $Inconclusive = if ($RunResult.InconclusiveCount -gt 0) { # $ReportTheme.Inconclusive # } # else { # $ReportTheme.Information # } # Try { # $PesterStatePassedScenariosCount = $PesterState.PassedScenarios.Count # } # Catch { # $PesterStatePassedScenariosCount = 0 # } # Try { # $PesterStateFailedScenariosCount = $PesterState.FailedScenarios.Count # } # Catch { # $PesterStateFailedScenariosCount = 0 # } # if ($ReportStrings.ContextsPassed) { # & $SafeCommands['Write-Host'] ($ReportStrings.ContextsPassed -f $PesterStatePassedScenariosCount) -Foreground $Success -NoNewLine # & $SafeCommands['Write-Host'] ($ReportStrings.ContextsFailed -f $PesterStateFailedScenariosCount) -Foreground $Failure # } if ($ReportStrings.TestsPassed) { & $SafeCommands['Write-Host'] ($ReportStrings.TestsPassed -f $RunResult.PassedCount) -Foreground $Success -NoNewLine & $SafeCommands['Write-Host'] ($ReportStrings.TestsFailed -f $RunResult.FailedCount) -Foreground $Failure -NoNewLine & $SafeCommands['Write-Host'] ($ReportStrings.TestsSkipped -f $RunResult.SkippedCount) -Foreground $Skipped -NoNewLine & $SafeCommands['Write-Host'] ($ReportStrings.TestsTotal -f $RunResult.TestsCount) -Foreground $Total -NoNewLine & $SafeCommands['Write-Host'] ($ReportStrings.TestsNotRun -f $RunResult.NotRunCount) -Foreground $NotRun # & $SafeCommands['Write-Host'] ($ReportStrings.TestsPending -f $RunResult.PendingCount) -Foreground $Pending -NoNewLine # & $SafeCommands['Write-Host'] ($ReportStrings.TestsInconclusive -f $RunResult.InconclusiveCount) -Foreground $Inconclusive } } function Write-CoverageReport { param ([object] $CoverageReport) if ($null -eq $CoverageReport -or ($pester.Show -eq [Pester.OutputTypes]::None) -or $CoverageReport.NumberOfCommandsAnalyzed -eq 0) { return } $totalCommandCount = $CoverageReport.NumberOfCommandsAnalyzed $fileCount = $CoverageReport.NumberOfFilesAnalyzed $executedPercent = ($CoverageReport.NumberOfCommandsExecuted / $CoverageReport.NumberOfCommandsAnalyzed).ToString("P2") $command = if ($totalCommandCount -gt 1) { $ReportStrings.CommandPlural } else { $ReportStrings.CommandSingular } $file = if ($fileCount -gt 1) { $ReportStrings.FilePlural } else { $ReportStrings.FileSingular } $commonParent = Get-CommonParentPath -Path $CoverageReport.AnalyzedFiles $report = $CoverageReport.MissedCommands | & $SafeCommands['Select-Object'] -Property @( @{ Name = 'File'; Expression = { Get-RelativePath -Path $_.File -RelativeTo $commonParent } } 'Class' 'Function' 'Line' 'Command' ) & $SafeCommands['Write-Host'] & $SafeCommands['Write-Host'] $ReportStrings.CoverageTitle -Foreground $ReportTheme.Coverage if ($CoverageReport.MissedCommands.Count -gt 0) { & $SafeCommands['Write-Host'] ($ReportStrings.CoverageMessage -f $command, $file, $executedPercent, $totalCommandCount, $fileCount) -Foreground $ReportTheme.CoverageWarn if ($CoverageReport.MissedCommands.Count -eq 1) { & $SafeCommands['Write-Host'] $ReportStrings.MissedSingular -Foreground $ReportTheme.CoverageWarn } else { & $SafeCommands['Write-Host'] $ReportStrings.MissedPlural -Foreground $ReportTheme.CoverageWarn } $report | & $SafeCommands['Format-Table'] -AutoSize | & $SafeCommands['Out-Host'] } else { & $SafeCommands['Write-Host'] ($ReportStrings.CoverageMessage -f $command, $file, $executedPercent, $totalCommandCount, $fileCount) -Foreground $ReportTheme.Coverage } } function ConvertTo-FailureLines { param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] $ErrorRecord, [switch] $ForceFullError ) process { $lines = [PSCustomObject] @{ Message = @() Trace = @() } ## convert the exception messages $exception = $ErrorRecord.Exception $exceptionLines = @() while ($exception) { $exceptionName = $exception.GetType().Name $thisLines = $exception.Message.Split([string[]]($([System.Environment]::NewLine), "`n"), [System.StringSplitOptions]::RemoveEmptyEntries) if (0 -lt @($thisLines).Count -and $ErrorRecord.FullyQualifiedErrorId -ne 'PesterAssertionFailed') { $thisLines[0] = "$exceptionName`: $($thisLines[0])" } [array]::Reverse($thisLines) $exceptionLines += $thisLines $exception = $exception.InnerException } [array]::Reverse($exceptionLines) $lines.Message += $exceptionLines if ($ErrorRecord.FullyQualifiedErrorId -eq 'PesterAssertionFailed') { $lines.Message += "at $($ErrorRecord.TargetObject.LineText.Trim()), $($ErrorRecord.TargetObject.File):$($ErrorRecord.TargetObject.Line)".Split([string[]]($([System.Environment]::NewLine), "`n"), [System.StringSplitOptions]::RemoveEmptyEntries) } if ( -not ($ErrorRecord | & $SafeCommands['Get-Member'] -Name ScriptStackTrace) ) { if ($ErrorRecord.FullyQualifiedErrorID -eq 'PesterAssertionFailed') { $lines.Trace += "at line: $($ErrorRecord.TargetObject.Line) in $($ErrorRecord.TargetObject.File)" } else { $lines.Trace += "at line: $($ErrorRecord.InvocationInfo.ScriptLineNumber) in $($ErrorRecord.InvocationInfo.ScriptName)" } return $lines } ## convert the stack trace if present (there might be none if we are raising the error ourselves) # todo: this is a workaround see https://github.com/pester/Pester/pull/886 if ($null -ne $ErrorRecord.ScriptStackTrace) { $traceLines = $ErrorRecord.ScriptStackTrace.Split([Environment]::NewLine, [System.StringSplitOptions]::RemoveEmptyEntries) } if ($ForceFullError -or $PesterPreference.Debug.ShowFullErrors.Value) { $lines.Trace += $traceLines } else { # omit the lines internal to Pester if ((GetPesterOS) -ne 'Windows') { [String]$pattern1 = '^at .*, .*/Pester.Runtime.psm1: line [0-9]*$' [String]$pattern2 = '^at (Invoke-Test|Context|Describe|InModuleScope), .*/Functions/.*.ps1: line [0-9]*$' [String]$pattern3 = '^at (Invoke-Pester), .*/.*.psm1: line [0-9]*$' [String]$pattern4 = '^at (Should<End>|Invoke-Assertion), .*/Functions/Assertions/Should.ps1: line [0-9]*$' [String]$pattern5 = '^at Assert-MockCalled, .*/Functions/Mock.ps1: line [0-9]*$' [String]$pattern6 = '^at (<ScriptBlock>|Invoke-Gherkin.*), (<No file>|.*/Functions/.*.ps1): line [0-9]*$' [String]$pattern7 = '^at Invoke-LegacyAssertion, .*/Functions/.*.ps1: line [0-9]*$' } else { [String]$pattern1 = '^at .*, .*\\Pester.Runtime.psm1: line [0-9]*$' [String]$pattern2 = '^at (Invoke-Test|Context|Describe|InModuleScope), .*\\Functions\\.*.ps1: line [0-9]*$' [String]$pattern3 = '^at (Invoke-Pester), .*\\.*.psm1: line [0-9]*$' [String]$pattern4 = '^at (Should<End>|Invoke-Assertion), .*\\Functions\\Assertions\\Should.ps1: line [0-9]*$' [String]$pattern5 = '^at Assert-MockCalled, .*\\Functions\\Mock.ps1: line [0-9]*$' [String]$pattern6 = '^at (<ScriptBlock>|Invoke-Gherkin.*), (<No file>|.*\\Functions\\.*.ps1): line [0-9]*$' [String]$pattern7 = '^at Invoke-LegacyAssertion, .*\\Functions\\.*.ps1: line [0-9]*$' } # reducing the stack trace so we see only stack trace until the current It block and not up until the invocation of the # whole test script itself. This is achieved by shortening the stack trace when any Runtime function is hit. # what we don't want to do here is shorten the stack on the Should or Invoke-Assertion. That would remove any # lines describing potential functions that are invoked in the test. e.g. doing function a() { 1 | Should -Be 2 }; a # we want to be able to see that we invoked the assertion inside of function a # the internal calls to Should and Invoke-Assertion are filtered out later by the second match foreach ($line in $traceLines) { if ($line -match $pattern1) { break } $isPesterInternalFunction = $line -match $pattern2 -or $line -match $pattern3 -or $line -match $pattern4 -or $line -match $pattern5 -or $line -match $pattern6 -or $line -match $pattern7 if (-not $isPesterInternalFunction) { $lines.Trace += $line } } } # make error navigateable in VSCode $lines.Trace = $lines.Trace -replace ':\s*line\s*(\d+)\s*$', ':$1' return $lines } } function ConvertTo-HumanTime { param ([TimeSpan]$TimeSpan) if ($TimeSpan.Ticks -lt [timespan]::TicksPerSecond) { "$([int]($TimeSpan.TotalMilliseconds))ms" } else { "$([int]($TimeSpan.TotalSeconds))s" } } function Get-WriteScreenPlugin { # add -FrameworkSetup Write-PesterStart $pester $Script and -FrameworkTeardown { $pester | Write-PesterReport } # The plugin is not imported when output None is specified so the usual level of output is Minimal. Pester.Runtime\New-PluginObject -Name "WriteScreen" ` -Start { param ($Context) # Write-PesterStart $Context } ` -DiscoveryStart { param ($Context) & $SafeCommands["Write-Host"] -ForegroundColor Magenta "`nStarting test discovery in $(@($Context.BlockContainers).Length) files." } ` -ContainerDiscoveryStart { param ($Context) if ('Normal' -eq $PesterPreference.Output.Verbosity.Value) { & $SafeCommands["Write-Host"] -ForegroundColor Magenta "Discovering tests in $($Context.BlockContainer.Content)." } } ` -ContainerDiscoveryEnd { param ($Context) if ('Normal' -eq $PesterPreference.Output.Verbosity.Value) { & $SafeCommands["Write-Host"] -ForegroundColor Magenta "Found $(@(View-Flat -Block $Context.Block).Count) tests. $(ConvertTo-HumanTime $Context.Duration)" } } ` -DiscoveryEnd { param ($Context) if ($Context.AnyFocusedTests) { $focusedTests = $Context.FocusedTests & $SafeCommands["Write-Host"] -ForegroundColor Magenta "There are some ($($focusedTests.Count)) focused tests '$($(foreach ($p in $focusedTests) { $p -join "." }) -join ",")' running just them." } & $SafeCommands["Write-Host"] -ForegroundColor Magenta "Test discovery finished. $(ConvertTo-HumanTime $Context.Duration)" } ` -ContainerRunStart { param ($Context) if ('Normal' -eq $PesterPreference.Output.Verbosity.Value) { if ("file" -eq $Context.Block.BlockContainer.Type) { # write two spaces to separate each file & $SafeCommands["Write-Host"] & $SafeCommands["Write-Host"] & $SafeCommands["Write-Host"] -ForegroundColor Magenta "Running tests from '$($Context.Block.BlockContainer.Content)'" } } } ` -ContainerRunEnd { param ($Context) if ($Context.Block.ErrorRecord.Count -gt 0) { & $SafeCommands["Write-Host"] -ForegroundColor Red "Container '$($Context.Block.BlockContainer.Content)' failed with:" Write-ErrorToScreen $Context.Block.ErrorRecord } } ` -EachBlockSetupStart { param ($Context) # the $context does not mean Context block, it's just a generic name # for the invocation context of this callback if ('Normal' -ne $PesterPreference.Output.Verbosity.Value) { return } $commandUsed = $Context.Block.FrameworkData.CommandUsed $block = $Context.Block # -1 moves the block closer to the start of theline $level = $block.Path.Length - 1 $margin = $ReportStrings.Margin * $level $text = $ReportStrings.$commandUsed -f $block.Name if ($PesterPreference.Debug.ShowNavigationMarkers.Value) { $text += ", $($block.ScriptBlock.File):$($block.ScriptBlock.StartPosition.StartLine)" } if (0 -eq $level -and -not $block.First) { # write extra line before top-level describe / context if it is not first # in that case there are already two spaces before the name of the file & $SafeCommands['Write-Host'] } & $SafeCommands['Write-Host'] "${margin}${Text}" -ForegroundColor $ReportTheme.$CommandUsed } -EachTestTeardownEnd { param ($Context) # we are currently in scope of describe so $Test is hardtyped and conflicts $_test = $Context.Test if ('Normal' -eq $PesterPreference.Output.Verbosity.Value) { $level = $_test.Path.Length $margin = $ReportStrings.Margin * ($level) $error_margin = $margin + $ReportStrings.Margin $out = $_test.ExpandedName } elseif ('Minimal' -eq $PesterPreference.Output.Verbosity.Value) { $level = 0 $margin = '' $error_margin = $ReportStrings.Margin $out = "$($_test.Block.Path -join '.').$($_test.ExpandedName)" } else { throw "Unsupported level out output '$($PesterPreference.Output.Verbosity.Value)'" } $humanTime = "$(Get-HumanTime ($_test.Duration + $_test.FrameworkDuration)) ($(Get-HumanTime $_test.Duration)|$(Get-HumanTime $_test.FrameworkDuration))" if ($PesterPreference.Debug.ShowNavigationMarkers.Value) { $out += ", $($_test.ScriptBlock.File):$($_Test.ScriptBlock.StartPosition.StartLine)" } $result = $_test.Result switch ($result) { Passed { if ('Normal' -eq $PesterPreference.Output.Verbosity.Value) { & $SafeCommands['Write-Host'] -ForegroundColor $ReportTheme.Pass "$margin[+] $out" -NoNewLine & $SafeCommands['Write-Host'] -ForegroundColor $ReportTheme.PassTime " $humanTime" } break } Failed { & $SafeCommands['Write-Host'] -ForegroundColor $ReportTheme.Fail "$margin[-] $out" -NoNewLine & $SafeCommands['Write-Host'] -ForegroundColor $ReportTheme.FailTime " $humanTime" # review how we should write errors for VS code based on https://github.com/PowerShell/vscode-powershell/pull/2447 # and use the env variable mentioned there # if($PesterPreference.Debug.WriteVSCodeMarker.Value) { # & $SafeCommands['Write-Host'] -ForegroundColor $ReportTheme.Fail $($_test.ErrorRecord[-1].DisplayStackTrace -replace '(?m)^',$error_margin) # & $SafeCommands['Write-Host'] -ForegroundColor $ReportTheme.Fail $($_test.ErrorRecord[-1].DisplayErrorMessage -replace '(?m)^',$error_margin) # } # else { Write-ErrorToScreen $_test.ErrorRecord -ErrorMargin $error_margin # } break } Skipped { if ('Normal' -eq $PesterPreference.Output.Verbosity.Value) { $because = if ($_test.FailureMessage) { ", because $($_test.FailureMessage)" } else { $null } & $SafeCommands['Write-Host'] -ForegroundColor $ReportTheme.Skipped "$margin[!] $out" -NoNewLine & $SafeCommands['Write-Host'] -ForegroundColor $ReportTheme.Skipped ", is skipped$because" -NoNewLine & $SafeCommands['Write-Host'] -ForegroundColor $ReportTheme.SkippedTime " $humanTime" } break } Pending { if ('Normal' -eq $PesterPreference.Output.Verbosity.Value) { $because = if ($_test.FailureMessage) { ", because $($_test.FailureMessage)" } else { $null } & $SafeCommands['Write-Host'] -ForegroundColor $ReportTheme.Pending "$margin[?] $out" -NoNewLine & $SafeCommands['Write-Host'] -ForegroundColor $ReportTheme.Pending ", is pending$because" -NoNewLine & $SafeCommands['Write-Host'] -ForegroundColor $ReportTheme.PendingTime " $humanTime" } break } Inconclusive { if ('Normal' -eq $PesterPreference.Output.Verbosity.Value) { $because = if ($_test.FailureMessage) { ", because $($_test.FailureMessage)" } else { $null } & $SafeCommands['Write-Host'] -ForegroundColor $ReportTheme.Inconclusive "$margin[?] $out" -NoNewLine & $SafeCommands['Write-Host'] -ForegroundColor $ReportTheme.Inconclusive ", is inconclusive$because" -NoNewLine & $SafeCommands['Write-Host'] -ForegroundColor $ReportTheme.InconclusiveTime " $humanTime" } break } default { if ('Normal' -eq $PesterPreference.Output.Verbosity.Value) { # TODO: Add actual Incomplete status as default rather than checking for null time. if ($null -eq $_test.Duration) { & $SafeCommands['Write-Host'] -ForegroundColor $ReportTheme.Incomplete "$margin[?] $out" -NoNewLine & $SafeCommands['Write-Host'] -ForegroundColor $ReportTheme.IncompleteTime " $humanTime" } } } } } -EachBlockTeardownEnd { param ($Context) if (-not $Context.Block.OwnPassed) { if ('Normal' -eq $PesterPreference.Output.Verbosity.Value) { $level = $Context.Block.Path.Length $margin = $ReportStrings.Margin * ($level) $error_margin = $margin + $ReportStrings.Margin } $level = 0 $margin = 0 $error_margin = $ReportStrings.Margin foreach ($e in $Context.Block.ErrorRecord) { ConvertTo-FailureLines $e } & $SafeCommands['Write-Host'] -ForegroundColor Red "[-] $($Context.Block.FrameworkData.CommandUsed) $($Context.Block.Path -join ".") failed" Write-ErrorToScreen $Context.Block.ErrorRecord $error_margin } } ` -End { param ( $Context ) Write-PesterReport $Context.TestRun } } function Write-ErrorToScreen { [CmdletBinding()] param ( [Parameter(Mandatory)] $Err, [string] $ErrorMargin ) $multipleErrors = 1 -lt $Err.Count $out = if ($multipleErrors) { $c = 0 $(foreach ($e in $Err) { $isFormattedError = $null -ne $e.DisplayErrorMessage "[$(($c++))] $(if ($isFormattedError){ $e.DisplayErrorMessage } else { $e.Exception })$(if ($null -ne $e.DisplayStackTrace) {"$([Environment]::NewLine)$($e.DisplayStackTrace)"})" }) -join [Environment]::NewLine } else { $isFormattedError = $null -ne $Err.DisplayErrorMessage "$(if ($isFormattedError){ $Err.DisplayErrorMessage } else { $Err.Exception })$(if ($isFormattedError) { if ($null -ne $Err.DisplayStackTrace) {"$([Environment]::NewLine)$($Err.DisplayStackTrace)"}} else { if ($null -ne $Err.ScriptStackTrace) {"$([Environment]::NewLine)$($Err.ScriptStackTrace)"}})" } $withMargin = ($out -split [Environment]::NewLine) -replace '(?m)^', $ErrorMargin -join [Environment]::NewLine & $SafeCommands['Write-Host'] -ForegroundColor $ReportTheme.Fail "$withMargin" } # SIG # Begin signature block # MIIcVgYJKoZIhvcNAQcCoIIcRzCCHEMCAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB # gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR # AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQU4elQLB5xDMoUarBzNHW4tJ1j # YjaggheFMIIFDjCCA/agAwIBAgIQCIQ1OU/QbU6rESO7M78utDANBgkqhkiG9w0B # AQsFADByMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYD # VQQLExB3d3cuZGlnaWNlcnQuY29tMTEwLwYDVQQDEyhEaWdpQ2VydCBTSEEyIEFz # c3VyZWQgSUQgQ29kZSBTaWduaW5nIENBMB4XDTIwMDEzMTAwMDAwMFoXDTIxMDEw # NTEyMDAwMFowSzELMAkGA1UEBhMCQ1oxDjAMBgNVBAcTBVByYWhhMRUwEwYDVQQK # DAxKYWt1YiBKYXJlxaExFTATBgNVBAMMDEpha3ViIEphcmXFoTCCASIwDQYJKoZI # hvcNAQEBBQADggEPADCCAQoCggEBALYF0cDtFUyYgraHpHdObGJM9dxjfRr0WaPN # kVZcEHdPXk4bVCPZLSca3Byybx745CpB3oejDHEbohLSTrbunoSA9utpwxVQSutt # /H1onVexiJgwGJ6xoQgR17FGLBGiIHgyPhFJhba9yENh0dqargLWllsg070WE2yb # gz3m659gmfuCuSZOhQ2nCHvOjEocTiI67mZlHvN7axg+pCgdEJrtIyvhHPqXeE2j # cdMrfmYY1lq2FBpELEW1imYlu5BnaJd/5IT7WjHL3LWx5Su9FkY5RwrA6+X78+j+ # vKv00JtDjM0dT+4A/m65jXSywxa4YoGDqQ5n+BwDMQlWCzfu37sCAwEAAaOCAcUw # ggHBMB8GA1UdIwQYMBaAFFrEuXsqCqOl6nEDwGD5LfZldQ5YMB0GA1UdDgQWBBRE # 05R/U5mVzc4vKq4rvKyyPm12EzAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYI # KwYBBQUHAwMwdwYDVR0fBHAwbjA1oDOgMYYvaHR0cDovL2NybDMuZGlnaWNlcnQu # Y29tL3NoYTItYXNzdXJlZC1jcy1nMS5jcmwwNaAzoDGGL2h0dHA6Ly9jcmw0LmRp # Z2ljZXJ0LmNvbS9zaGEyLWFzc3VyZWQtY3MtZzEuY3JsMEwGA1UdIARFMEMwNwYJ # YIZIAYb9bAMBMCowKAYIKwYBBQUHAgEWHGh0dHBzOi8vd3d3LmRpZ2ljZXJ0LmNv # bS9DUFMwCAYGZ4EMAQQBMIGEBggrBgEFBQcBAQR4MHYwJAYIKwYBBQUHMAGGGGh0 # dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBOBggrBgEFBQcwAoZCaHR0cDovL2NhY2Vy # dHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0U0hBMkFzc3VyZWRJRENvZGVTaWduaW5n # Q0EuY3J0MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggEBADAk7PRuDcdl # lPZQSfZ1Y0jeItmEWPMNcAL0LQaa6M5Slrznjxv1ZiseT9SMWTxOQylfPvpOSo1x # xV3kD7qf7tf2EuicKkV6dBgGiHb0riWZ3+wMA6C8IK3cGesJ4jgpTtYEzbh88pxT # g2MSzpRnwyXHhrgcKSps1z34JmmmHP1lncxNC6DTM6yEUwE7XiDD2xNoeLITgdTQ # jjMMT6nDJe8+xL0Zyh32OPIyrG7qPjG6MmEjzlCaWsE/trVo7I9CSOjwpp8721Hj # q/tIHzPFg1C3dYmDh8Kbmr21dHWBLYQF4P8lq8u8AYDa6H7xvkx7G0i2jglAA4YK # i1V8AlyTwRkwggUwMIIEGKADAgECAhAECRgbX9W7ZnVTQ7VvlVAIMA0GCSqGSIb3 # DQEBCwUAMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAX # BgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNVBAMTG0RpZ2lDZXJ0IEFzc3Vy # ZWQgSUQgUm9vdCBDQTAeFw0xMzEwMjIxMjAwMDBaFw0yODEwMjIxMjAwMDBaMHIx # CzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3 # dy5kaWdpY2VydC5jb20xMTAvBgNVBAMTKERpZ2lDZXJ0IFNIQTIgQXNzdXJlZCBJ # RCBDb2RlIFNpZ25pbmcgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB # AQD407Mcfw4Rr2d3B9MLMUkZz9D7RZmxOttE9X/lqJ3bMtdx6nadBS63j/qSQ8Cl # +YnUNxnXtqrwnIal2CWsDnkoOn7p0WfTxvspJ8fTeyOU5JEjlpB3gvmhhCNmElQz # UHSxKCa7JGnCwlLyFGeKiUXULaGj6YgsIJWuHEqHCN8M9eJNYBi+qsSyrnAxZjNx # PqxwoqvOf+l8y5Kh5TsxHM/q8grkV7tKtel05iv+bMt+dDk2DZDv5LVOpKnqagqr # hPOsZ061xPeM0SAlI+sIZD5SlsHyDxL0xY4PwaLoLFH3c7y9hbFig3NBggfkOItq # cyDQD2RzPJ6fpjOp/RnfJZPRAgMBAAGjggHNMIIByTASBgNVHRMBAf8ECDAGAQH/ # AgEAMA4GA1UdDwEB/wQEAwIBhjATBgNVHSUEDDAKBggrBgEFBQcDAzB5BggrBgEF # BQcBAQRtMGswJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBD # BggrBgEFBQcwAoY3aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0 # QXNzdXJlZElEUm9vdENBLmNydDCBgQYDVR0fBHoweDA6oDigNoY0aHR0cDovL2Ny # bDQuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENBLmNybDA6oDig # NoY0aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9v # dENBLmNybDBPBgNVHSAESDBGMDgGCmCGSAGG/WwAAgQwKjAoBggrBgEFBQcCARYc # aHR0cHM6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzAKBghghkgBhv1sAzAdBgNVHQ4E # FgQUWsS5eyoKo6XqcQPAYPkt9mV1DlgwHwYDVR0jBBgwFoAUReuir/SSy4IxLVGL # p6chnfNtyA8wDQYJKoZIhvcNAQELBQADggEBAD7sDVoks/Mi0RXILHwlKXaoHV0c # LToaxO8wYdd+C2D9wz0PxK+L/e8q3yBVN7Dh9tGSdQ9RtG6ljlriXiSBThCk7j9x # jmMOE0ut119EefM2FAaK95xGTlz/kLEbBw6RFfu6r7VRwo0kriTGxycqoSkoGjpx # KAI8LpGjwCUR4pwUR6F6aGivm6dcIFzZcbEMj7uo+MUSaJ/PQMtARKUT8OZkDCUI # QjKyNookAv4vcn4c10lFluhZHen6dGRrsutmQ9qzsIzV6Q3d9gEgzpkxYz0IGhiz # gZtPxpMQBvwHgfqL2vmCSfdibqFT+hKUGIUukpHqaGxEMrJmoecYpJpkUe8wggZq # MIIFUqADAgECAhADAZoCOv9YsWvW1ermF/BmMA0GCSqGSIb3DQEBBQUAMGIxCzAJ # BgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5k # aWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IEFzc3VyZWQgSUQgQ0EtMTAe # Fw0xNDEwMjIwMDAwMDBaFw0yNDEwMjIwMDAwMDBaMEcxCzAJBgNVBAYTAlVTMREw # DwYDVQQKEwhEaWdpQ2VydDElMCMGA1UEAxMcRGlnaUNlcnQgVGltZXN0YW1wIFJl # c3BvbmRlcjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKNkXfx8s+CC # NeDg9sYq5kl1O8xu4FOpnx9kWeZ8a39rjJ1V+JLjntVaY1sCSVDZg85vZu7dy4Xp # X6X51Id0iEQ7Gcnl9ZGfxhQ5rCTqqEsskYnMXij0ZLZQt/USs3OWCmejvmGfrvP9 # Enh1DqZbFP1FI46GRFV9GIYFjFWHeUhG98oOjafeTl/iqLYtWQJhiGFyGGi5uHzu # 5uc0LzF3gTAfuzYBje8n4/ea8EwxZI3j6/oZh6h+z+yMDDZbesF6uHjHyQYuRhDI # jegEYNu8c3T6Ttj+qkDxss5wRoPp2kChWTrZFQlXmVYwk/PJYczQCMxr7GJCkawC # wO+k8IkRj3cCAwEAAaOCAzUwggMxMA4GA1UdDwEB/wQEAwIHgDAMBgNVHRMBAf8E # AjAAMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMIIBvwYDVR0gBIIBtjCCAbIwggGh # BglghkgBhv1sBwEwggGSMCgGCCsGAQUFBwIBFhxodHRwczovL3d3dy5kaWdpY2Vy # dC5jb20vQ1BTMIIBZAYIKwYBBQUHAgIwggFWHoIBUgBBAG4AeQAgAHUAcwBlACAA # bwBmACAAdABoAGkAcwAgAEMAZQByAHQAaQBmAGkAYwBhAHQAZQAgAGMAbwBuAHMA # dABpAHQAdQB0AGUAcwAgAGEAYwBjAGUAcAB0AGEAbgBjAGUAIABvAGYAIAB0AGgA # ZQAgAEQAaQBnAGkAQwBlAHIAdAAgAEMAUAAvAEMAUABTACAAYQBuAGQAIAB0AGgA # ZQAgAFIAZQBsAHkAaQBuAGcAIABQAGEAcgB0AHkAIABBAGcAcgBlAGUAbQBlAG4A # dAAgAHcAaABpAGMAaAAgAGwAaQBtAGkAdAAgAGwAaQBhAGIAaQBsAGkAdAB5ACAA # YQBuAGQAIABhAHIAZQAgAGkAbgBjAG8AcgBwAG8AcgBhAHQAZQBkACAAaABlAHIA # ZQBpAG4AIABiAHkAIAByAGUAZgBlAHIAZQBuAGMAZQAuMAsGCWCGSAGG/WwDFTAf # BgNVHSMEGDAWgBQVABIrE5iymQftHt+ivlcNK2cCzTAdBgNVHQ4EFgQUYVpNJLZJ # Mp1KKnkag0v0HonByn0wfQYDVR0fBHYwdDA4oDagNIYyaHR0cDovL2NybDMuZGln # aWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEQ0EtMS5jcmwwOKA2oDSGMmh0dHA6 # Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJRENBLTEuY3JsMHcG # CCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQu # Y29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGln # aUNlcnRBc3N1cmVkSURDQS0xLmNydDANBgkqhkiG9w0BAQUFAAOCAQEAnSV+GzNN # siaBXJuGziMgD4CH5Yj//7HUaiwx7ToXGXEXzakbvFoWOQCd42yE5FpA+94GAYw3 # +puxnSR+/iCkV61bt5qwYCbqaVchXTQvH3Gwg5QZBWs1kBCge5fH9j/n4hFBpr1i # 2fAnPTgdKG86Ugnw7HBi02JLsOBzppLA044x2C/jbRcTBu7kA7YUq/OPQ6dxnSHd # FMoVXZJB2vkPgdGZdA0mxA5/G7X1oPHGdwYoFenYk+VVFvC7Cqsc21xIJ2bIo4sK # HOWV2q7ELlmgYd3a822iYemKC23sEhi991VUQAOSK2vCUcIKSK+w1G7g9BQKOhvj # jz3Kr2qNe9zYRDCCBs0wggW1oAMCAQICEAb9+QOWA63qAArrPye7uhswDQYJKoZI # hvcNAQEFBQAwZTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZ # MBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEkMCIGA1UEAxMbRGlnaUNlcnQgQXNz # dXJlZCBJRCBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTIxMTExMDAwMDAwMFow # YjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQ # d3d3LmRpZ2ljZXJ0LmNvbTEhMB8GA1UEAxMYRGlnaUNlcnQgQXNzdXJlZCBJRCBD # QS0xMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6IItmfnKwkKVpYBz # QHDSnlZUXKnE0kEGj8kz/E1FkVyBn+0snPgWWd+etSQVwpi5tHdJ3InECtqvy15r # 7a2wcTHrzzpADEZNk+yLejYIA6sMNP4YSYL+x8cxSIB8HqIPkg5QycaH6zY/2DDD # /6b3+6LNb3Mj/qxWBZDwMiEWicZwiPkFl32jx0PdAug7Pe2xQaPtP77blUjE7h6z # 8rwMK5nQxl0SQoHhg26Ccz8mSxSQrllmCsSNvtLOBq6thG9IhJtPQLnxTPKvmPv2 # zkBdXPao8S+v7Iki8msYZbHBc63X8djPHgp0XEK4aH631XcKJ1Z8D2KkPzIUYJX9 # BwSiCQIDAQABo4IDejCCA3YwDgYDVR0PAQH/BAQDAgGGMDsGA1UdJQQ0MDIGCCsG # AQUFBwMBBggrBgEFBQcDAgYIKwYBBQUHAwMGCCsGAQUFBwMEBggrBgEFBQcDCDCC # AdIGA1UdIASCAckwggHFMIIBtAYKYIZIAYb9bAABBDCCAaQwOgYIKwYBBQUHAgEW # Lmh0dHA6Ly93d3cuZGlnaWNlcnQuY29tL3NzbC1jcHMtcmVwb3NpdG9yeS5odG0w # ggFkBggrBgEFBQcCAjCCAVYeggFSAEEAbgB5ACAAdQBzAGUAIABvAGYAIAB0AGgA # aQBzACAAQwBlAHIAdABpAGYAaQBjAGEAdABlACAAYwBvAG4AcwB0AGkAdAB1AHQA # ZQBzACAAYQBjAGMAZQBwAHQAYQBuAGMAZQAgAG8AZgAgAHQAaABlACAARABpAGcA # aQBDAGUAcgB0ACAAQwBQAC8AQwBQAFMAIABhAG4AZAAgAHQAaABlACAAUgBlAGwA # eQBpAG4AZwAgAFAAYQByAHQAeQAgAEEAZwByAGUAZQBtAGUAbgB0ACAAdwBoAGkA # YwBoACAAbABpAG0AaQB0ACAAbABpAGEAYgBpAGwAaQB0AHkAIABhAG4AZAAgAGEA # cgBlACAAaQBuAGMAbwByAHAAbwByAGEAdABlAGQAIABoAGUAcgBlAGkAbgAgAGIA # eQAgAHIAZQBmAGUAcgBlAG4AYwBlAC4wCwYJYIZIAYb9bAMVMBIGA1UdEwEB/wQI # MAYBAf8CAQAweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2Nz # cC5kaWdpY2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2lj # ZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcnQwgYEGA1UdHwR6MHgw # OqA4oDaGNGh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJ # RFJvb3RDQS5jcmwwOqA4oDaGNGh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9EaWdp # Q2VydEFzc3VyZWRJRFJvb3RDQS5jcmwwHQYDVR0OBBYEFBUAEisTmLKZB+0e36K+ # Vw0rZwLNMB8GA1UdIwQYMBaAFEXroq/0ksuCMS1Ri6enIZ3zbcgPMA0GCSqGSIb3 # DQEBBQUAA4IBAQBGUD7Jtygkpzgdtlspr1LPUukxR6tWXHvVDQtBs+/sdR90OPKy # XGGinJXDUOSCuSPRujqGcq04eKx1XRcXNHJHhZRW0eu7NoR3zCSl8wQZVann4+er # Ys37iy2QwsDStZS9Xk+xBdIOPRqpFFumhjFiqKgz5Js5p8T1zh14dpQlc+Qqq8+c # dkvtX8JLFuRLcEwAiR78xXm8TBJX/l/hHrwCXaj++wc4Tw3GXZG5D2dFzdaD7eeS # DY2xaYxP+1ngIw/Sqq4AfO6cQg7PkdcntxbuD8O9fAqg7iwIVYUiuOsYGk38KiGt # STGDR5V3cdyxG0tLHBCcdxTBnU8vWpUIKRAmMYIEOzCCBDcCAQEwgYYwcjELMAkG # A1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRp # Z2ljZXJ0LmNvbTExMC8GA1UEAxMoRGlnaUNlcnQgU0hBMiBBc3N1cmVkIElEIENv # ZGUgU2lnbmluZyBDQQIQCIQ1OU/QbU6rESO7M78utDAJBgUrDgMCGgUAoHgwGAYK # KwYBBAGCNwIBDDEKMAigAoAAoQKAADAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIB # BDAcBgorBgEEAYI3AgELMQ4wDAYKKwYBBAGCNwIBFTAjBgkqhkiG9w0BCQQxFgQU # 9RjQod8A6wP5OokVyyekTpVqyeQwDQYJKoZIhvcNAQEBBQAEggEAR1yBvoYbI3IZ # Xz63INv540jub0kRz1HS8I3Cu6IhSPbNVFAl/fXdco37+AzLzaJeQMNH2Q2tUU5U # E1fb5BuvKF9pWE9LBhAoY1qQSJxwxuhiEAaQarg61SO+2e9I5KiM3wDq235IfjAR # v4KrlwNFzlRDYrqFMVfukii00iJgeHiCGFWLKcNR5S/D5JOUe7MQ1g24E66N4BIY # DsNHo88XnzTMgdrkLI+id/yVOW6DNmB7QiQEwt+JyG37zWNlHpgQqgBa6pDbeJ3y # /iXQlE/FPciZ+1lANrbKOfG49+xdcYY30YSwnZPZq1Mve0dzlyGIPRpknO7jiknT # 2Wc1+HKRl6GCAg8wggILBgkqhkiG9w0BCQYxggH8MIIB+AIBATB2MGIxCzAJBgNV # BAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdp # Y2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IEFzc3VyZWQgSUQgQ0EtMQIQAwGa # Ajr/WLFr1tXq5hfwZjAJBgUrDgMCGgUAoF0wGAYJKoZIhvcNAQkDMQsGCSqGSIb3 # DQEHATAcBgkqhkiG9w0BCQUxDxcNMjAwMjA4MjMyODU2WjAjBgkqhkiG9w0BCQQx # FgQUJvrrxNypNlsT9zg7xf2BjSy4B/kwDQYJKoZIhvcNAQEBBQAEggEAVqABu7DN # p9m2nDCbIlpJWhZRBDe33shXNVe1Pdk/SLMFZ6dlyFT51PdFr15kwbZj0sPL7fqW # CzXsp/f813USZ3CjZRirJjXRDMBlyy63vPKf5QcRRPVSQ85BxOPx/HyIIxYmgaRy # 0a/do34zvBh20NeBXtUkbJw6kf0z2A9KC2D35Gtw7vghTSZd1szj/GgDKXoPObBP # +ikRHlCKT1STtZN2NnTbUVPU4ppLPh2SbspSoArUeeJrgFTCr6qLzPqUooMO9jTK # KI+wGuFkD445P5L0ghSBaoCCfq0CgutCy+SefJd66cF613WiI4yUZH5DUspOMi// # 3EHNFy2gPo7khw== # SIG # End signature block |