Chapter 14. Debugging

Introduction

While developing scripts and functions, you'll often find yourself running into behavior that you didn't intend. This is a natural part of software development, and the path to diagnosing these issues is the fine art known as debugging.

For the simplest of problems, a well-placed call to Write-Host can answer many of your questions. Did your script get to the places you thought it should? Were the variables set to the values you thought they should be?

Once problems get more complex, print-style debugging quickly becomes cumbersome and unwieldy. Rather than continually modifying your script to diagnose its behavior, you can leverage PowerShell's much more extensive debugging facilities to help you get to the root of the problem.

1 comment

  1. AndrewTearle Posted 15 days and 19 hours ago

    becomes cumbersome and unweildy. <

Add a comment

PS > Set-PsBreakPoint .\Invoke-ComplexDebuggerScript.ps1 -Line 14

  ID Script           Line Command         Variable        Action
  -- ------           ---- -------         --------        ------
   0 Invoke-Comple...   14


PS > .\Invoke-ComplexDebuggerScript.ps1
Calculating lots of complex information
1225
89
Entering debug mode. Use h or ? for help.

Hit Line breakpoint on
'Z:\Documents\CookbookV2\chapters\current\PowerShellCookbook\Invoke-Comple
xDebuggerScript.ps1:14'

Invoke-ComplexDebuggerScript.ps1:14      $dirCount = 0
PS > ?

 s, stepInto         Single step (step into functions, scripts, etc.)
 v, stepOver         Step to next statement (step over functions, scripts,
etc.)
 o, stepOut          Step out of the current function, script, etc.

 c, continue         Continue execution
 q, quit             Stop execution and exit the debugger

 k, Get-PSCallStack  Display call stack

 l, list             List source code for the current script.
                     Use "list" to start from the current line, "list <m>"

                     to start from line <m>, and "list <m> <n>" to list <n>

                     lines starting from line <m>

 <enter>             Repeat last command if it was stepInto, stepOver or li
st

 ?, h                Displays this help message


For instructions about how to customize your debugger prompt, type "help ab
out_prompt".

PS > k

Command                  Arguments                Location
-------                  ---------                --------
HelperFunction           {}                       Invoke-ComplexDebugge...
Invoke-ComplexDebugge... {}                       Invoke-ComplexDebugge...
prompt                   {}                       prompt

By leveraging strict mode, you can often save yourself from writing bugs in the first place. Once you discover an issue, script tracing can help you get a quick overview of the execution flow taken by your script. For interactive diagnosis, PowerShell's Integrated Scripting Environment (ISE) offers full-featured graphical debugging support. From the command-line, the *-PsBreakPoint cmdlets let you investigate your script when it hits a specific line, condition, or error.

Prevent Common Scripting Errors

Problem

You want to have PowerShell warn you when your script contains an error likely to result in a bug.

Solution

Use the Set-StrictMode cmdlet to place PowerShell in a mode that prevents many of the scripting errors that tend to introduce bugs.

PS > function BuggyFunction
>> {
>>     $testVariable = "Hello"
>>     if($testVariab1e -eq "Hello")
>>     {
>>         "Should get here"
>>     }
>>     else
>>     {
>>         "Should not get here"
>>     }
>> }
>>
PS > BuggyFunction
Should not get here

PS > Set-StrictMode -Version Latest
PS > BuggyFunction
The variable '$testVariab1e' cannot be retrieved because it has not been set.
At line:4 char:21
+     if($testVariab1e <<<<  -eq "Hello")
    + CategoryInfo          : InvalidOperation: (testVariab1e:Token) [], RuntimeException
    + FullyQualifiedErrorId : VariableIsUndefined

Discussion

By default, PowerShell allows you to assign data to variables you haven't yet created (thereby creating those variables). It also allows you to retrieve data from variables that don't exist—which usually happens by accident and almost always causes bugs. The solution demonstrates this trap, where the L in "variable" was accidentally replaced by the number 1.

To help save you from getting stung by this problem and others like it, PowerShell provides a strict mode that generates an error if you attempt to access a nonexisting variable. Example 14.1, “PowerShell operating in strict mode” demonstrates this mode.

Example 14.1. PowerShell operating in strict mode

PS > $testVariable = "Hello"
PS > $tsetVariable += " World"
PS > $testVariable
Hello
PS > Remove-Item Variable:\tsetvariable
PS > Set-StrictMode -Version Latest
PS > $testVariable = "Hello"
PS > $tsetVariable += " World"
The variable '$tsetVariable' cannot be retrieved because it has not been set.
At line:1 char:14
+ $tsetVariable <<<<  += "World"
    + CategoryInfo          : InvalidOperation: (tsetVariable:Token) [], RuntimeException
    + FullyQualifiedErrorId : VariableIsUndefined

In addition to saving you from accessing non-existent variables, strict mode also detects:

One unique feature of the Set-StrictMode cmdlet is the -Version parameter. As PowerShell releases new versions of the Set-StrictMode cmdlet, the cmdlet will become more powerful and detect additional scripting errors. Because of this, a script that works with one version of strict mode might no longer work under a later version. If you won't have the flexibility to modify your script to account for new strict mode rules, use "-Version 2" as the value of the -Version parameter.

Note

The Set-StrictMode cmdlet is scoped, meaning that the strict mode set in one script or function doesn't impact the scripts or functions that call it. To temporarily disable strict mode for a region of script, do so in a new script block:

& { Set-StrictMode -Off; $tsetVariable }

For the sake of your script debugging health and sanity, strict mode should be one of the first additions you make to your PowerShell profile.

1 comment

  1. David "Makovec" Moravec Posted 14 days and 7 hours ago

    What about to provide link to chapter about profile customizing.

Add a comment

Trace Script Execution

Problem

You want to review the flow of execution taken by your script as PowerShell runs it.

Solution

Use the -Trace parameter of the Set-PsDebug cmdlet to have PowerShell trace your script as it executes it.

PS > function BuggyFunction
>> {
>>     $testVariable = "Hello"
>>     if($testVariab1e -eq "Hello")
>>     {
>>         "Should get here"
>>     }
>>     else
>>     {
>>         "Should not get here"
>>     }
>> }
>>
PS > Set-PsDebug -Trace 1
PS > BuggyFunction
DEBUG:    1+  <<<< BuggyFunction
DEBUG:    3+     $testVariable = <<<<  "Hello"
DEBUG:    4+     if <<<< ($testVariab1e -eq "Hello")
DEBUG:   10+         "Should not get here" <<<<
Should not get here

Discussion

When it comes to simple interactive debugging (as opposed to bug prevention), PowerShell supports several of the most useful debugging features that you might be accustomed to. For the full experience, the Integrated Scripting Environment (ISE) offers a full-fledged graphical debugger. For more information about debugging in the ISE, see the section called “Debug a Script”.

From the command-line, though you still have access to tracing (through the Set-PsDebug -Trace statement), stepping (through the Set-PsDebug -Step statement), and environment inspection (through the $host.EnterNestedPrompt() call). While the *-PsBreakpoint cmdlets support much more functionality in addition to these primitives, the Set-PsDebug cmdlet is useful for some simple problems.

As a demonstration of these techniques, consider Example 14.2, “A complex script that interacts with PowerShell's debugging features”.

Example 14.2. A complex script that interacts with PowerShell's debugging features

#############################################################################

Write-Host "Calculating lots of complex information"

$runningTotal = 0
$runningTotal += [Math]::Pow(5 * 5 + 10, 2)

Write-Debug "Current value: $runningTotal"

Set-PsDebug -Trace 1
$dirCount = @(Get-ChildItem $env:WINDIR).Count

Set-PsDebug -Trace 2
$runningTotal -= 10
$runningTotal /= 2

Set-PsDebug -Step
$runningTotal *= 3
$runningTotal /= 2

$host.EnterNestedPrompt()

Set-PsDebug -off

As you try to determine why this script isn't working as you expect, a debugging session might look like Example 14.3, “Debugging a complex script”.

Example 14.3. Debugging a complex script

PS > $debugPreference = "Continue"
PS > Invoke-ComplexScript.ps1
Calculating lots of complex information
DEBUG: Current value: 1225
DEBUG:   17+ $dirCount = @(Get-ChildItem $env:WINDIR).Count
DEBUG:   17+ $dirCount = @(Get-ChildItem $env:WINDIR).Count
DEBUG:   19+ Set-PsDebug -Trace 2
DEBUG:   20+ $runningTotal -= 10
DEBUG:     ! SET $runningTotal = '1215'.
DEBUG:   21+ $runningTotal /= 2
DEBUG:     ! SET $runningTotal = '607.5'.
DEBUG:   23+ Set-PsDebug -Step

Continue with this operation?
  24+ $runningTotal *= 3
[Y] Yes  [A] Yes to All  [N] No  [L] No to All  [S] Suspend [?] Help
(default is "Y"):y
DEBUG:   24+ $runningTotal *= 3
DEBUG:    !  SET $runningTotal = '1822.5'.

Continue with this operation?
  25+ $runningTotal /= 2
[Y] Yes  [A] Yes to All  [N] No  [L] No to All  [S] Suspend [?] Help
(default is "Y"):y
DEBUG:   25+ $runningTotal /= 2
DEBUG:    !  SET $runningTotal = '911.25'.

Continue with this operation?
  27+ $host.EnterNestedPrompt()
[Y] Yes  [A] Yes to All  [N] No  [L] No to All  [S] Suspend [?] Help
(default is "Y"):y
DEBUG:   27+ $host.EnterNestedPrompt()
DEBUG:     ! CALL method 'System.Void EnterNestedPrompt()'
PS > $dirCount
296
PS > $dirCount + $runningTotal
1207.25
PS > exit

Continue with this operation?
  29+ Set-PsDebug -off
[Y] Yes  [A] Yes to All  [N] No  [L] No to All  [S] Suspend [?] Help
(default is "Y"):y
DEBUG:   29+ Set-PsDebug -off

Together, these interactive debugging features are bound to help you diagnose and resolve simple problems quickly. For more complex problems, PowerShell's graphical debugger (in the ISE) and the *-PsBreakpoint cmdlets are here to help.

For more information about the Set-PsDebug cmdlet, type Get-Help Set-PsDebug. For more information about setting script breakpoints, see the section called “Set a Script Breakpoint”.

Set a Script Breakpoint

Problem

You want PowerShell to enter debugging mode when it executes a specific command, line in your script, or updates a variable.

Solution

Use the Set-PsBreakpoint cmdlet to set a new breakpoint:

Set-PsBreakPoint .\Invoke-ComplexDebuggerScript.ps1 -Line 21
Set-PSBreakpoint -Command Get-ChildItem
Set-PsBreakPoint -Variable dirCount

Discussion

When running a script, a breakpoint is a location (or condition) that causes PowerShell to temporarily pause execution of that script. When it does so, it enters debugging mode. Debugging mode lets you investigate the state of the script, and also gives you fine-grained control over the script's execution.

For more information about interacting with PowerShell's debugging mode, see the section called “Investigate System State While Debugging”.

The Set-PsBreakpoint cmdlet supports three primary types of breakpoints:

Positional

Positional breakpoints (lines, and optionally columns) cause PowerShell to pause execution once it reaches the specified location in the script you identify.

PS > Set-PSBreakpoint -Script .\Invoke-ComplexDebuggerScript.ps1 -Line 21

ID Script                           Line Command Variable Action
-- ------                           ---- ------- -------- ------
 0 Invoke-ComplexDebuggerScript.ps1   21


PS > .\Invoke-ComplexDebuggerScript.ps1
Calculating lots of complex information
Entering debug mode. Use h or ? for help.

Hit Line breakpoint on
'(...)\Invoke-ComplexDebuggerScript.ps1:21'

Invoke-ComplexDebuggerScript.ps1:21  $runningTotal

When running the debugger from the command line, you can use the section called “Program: Show Colorized Script Content” to determine script line numbers.

Command

Command breakpoints cause PowerShell to pause execution before calling the specified command. This is especially helpful for diagnosing in-memory functions, or for pausing before your script invokes a cmdlet. If you specify the -Script parameter, PowerShell only pauses when the command is either defined by that script (as in the case of dot-sourced functions), or called by that script. Although command breakpoints do not support the -Line parameter, you can get the same effect by setting a positional breakpoint on the script that defines them.

PS > Show-ColorizedContent $profile.CurrentUserAllHosts

(...)
084 | function grep(
085 |    [string] $text = $(throw "Specify a search string"),
086 |    [string] $filter = "*",
087 |    [switch] $rec,
088 |    [switch] $edit
089 |    )
090 | {
091 |    $results = & {
092 |       if($rec) { gci . $filter -rec | select-string $text }
093 |       else {gci $filter | select-string $text }
094 |    }
095 |    $results
096 | }
(...)

PS > Set-PsBreakpoint $profile.CurrentUserAllHosts -Line 92 -Column 18

  ID Script                      Line Command Variable
  -- ------                      ---- ------- --------
   0 profile.ps1                   92


PS > grep "function grep" *.ps1 -rec
Entering debug mode. Use h or ? for help.

Hit Line breakpoint on 'E:\Lee\WindowsPowerShell\profile.ps1:92, 18'

profile.ps1:92        if($rec) { gci . $filter -rec | select-string $text }

(...)
Variable

By default, variable breakpoints cause PowerShell to pause execution before changing the value of a variable.

PS > Set-PsBreakPoint -Variable dirCount

ID Script Line Command Variable Action
-- ------ ---- ------- -------- ------
 0                     dirCount

PS > .\Invoke-ComplexDebuggerScript.ps1
Calculating lots of complex information
1225
Entering debug mode. Use h or ? for help.

Hit Variable breakpoint on '$dirCount' (Write access)

Invoke-ComplexDebuggerScript.ps1:23
$dirCount = @(Get-ChildItem $env:WINDIR).Count
PS > 

In addition to letting you break before it changes the value of a variable, PowerShell also lets you break before it accesses the value of a variable.

Once you have a breakpoint defined, you can use the Disable-PsBreakpoint and Enable-PsBreakpoint cmdlets to control how PowerShell reacts to those breakpoints. If a breakpoint is disabled, PowerShell does not pause execution when it reaches that breakpoint. To remove a breakpoint completely, use the Remove-PsBreakpoint cmdlet.

In addition to interactive debugging, PowerShell also lets you define actions to perform automatically when it reaches a breakpoint. For more information, see the section called “Create a Conditional Breakpoint”.

For more information about PowerShell's debugging support, Get-Help about_Debuggers.

Debug a Script when it Encounters an Error

Problem

You want PowerShell to enter debugging mode as soon as it encounters an error.

Solution

Run the Enable-BreakOnError script to have PowerShell automatically pause script execution when it encounters an error:

Example 14.4. Enable-BreakOnError.ps1


$GLOBAL:EnableBreakOnErrorLastErrorCount = $error.Count

Set-PSBreakpoint -Command Out-Default -Action {

    if($error.Count -ne $EnableBreakOnErrorLastErrorCount)
    {
        $GLOBAL:EnableBreakOnErrorLastErrorCount = $error.Count
        break
    }
} 

Discussion

When PowerShell generates an error, its final action is displaying that error to you. This goes through the Out-Default cmdlet, as does all other PowerShell output. Knowing this, Example 14.4, “Enable-BreakOnError.ps1” defines a conditional breakpoint. That breakpoint fires only when the number of errors in the global $error collection changes from the last time it checked.

If you don't want PowerShell to break on all errors, you might just want to set a breakpoint on the last error you encountered. For that, run Set-PsBreakpointLastError and then run your script again:

Example 14.5. Set-PsBreakpointLastError.ps1

$lastError = $error[0]
Set-PsBreakpoint $lastError.InvocationInfo.ScriptName `
    $lastError.InvocationInfo.ScriptLineNumber
 

For more information about intercepting stages of the PowerShell pipeline via the Out-Default cmdlet, see the section called “Intercept Stages of the Pipeline”. For more information about conditional breakpoints, see the section called “Create a Conditional Breakpoint”.

For more information about PowerShell's debugging support, Get-Help about_Debuggers.

Create a Conditional Breakpoint

Problem

You want PowerShell to enter debugging mode when it encounters a breakpoint, but only when certain other conditions hold true as well.

Solution

Use the -Action parameter to define an action that PowerShell should take when it encounters the breakpoint. If the action includes a break statement, PowerShell pauses execution and enters debugging mode.

PS > Get-Content .\looper.ps1
for($count = 0; $count -lt 10; $count++)
{
    "Count is: $count"
}
PS > Set-PsBreakpoint .\looper.ps1 -Line 3 -Action {
>>     if($count -eq 4) { break }
>> }
>>

  ID Script           Line Command         Variable        Action
  -- ------           ---- -------         --------        ------
   0 looper.ps1          3                                 ...


PS > .\looper.ps1
Count is: 0
Count is: 1
Count is: 2
Count is: 3
Entering debug mode. Use h or ? for help.

Hit Line breakpoint on 'C:\temp\looper.ps1:3'

looper.ps1:3       "Count is: $count"
PS > $count
4
PS > c
Count is: 4
Count is: 5
Count is: 6
Count is: 7
Count is: 8
Count is: 9

Discussion

Conditional breakpoints are a great way to automate repetitive interactive debugging. When you are debugging an often-executed portion of your script, the problematic behavior often doesn't occur until that portion of your script has been executed hundreds or thousands of times. By narrowing down the conditions under which the breakpoint should apply (such as the value of an interesting variable), you can drastically simplify your debugging experience.

The solution demonstrates a conditional breakpoint that triggers only when the value of the $count variable is 4. When the -Action script block executes a break statement, PowerShell enters debug mode.

Inside the -Action script block, you have access to all variables that exist at that time. You can review them, or even change them if desired.

In addition to being useful for conditional breakpoints, the -Action script block also proves helpful for generalized logging or automatic debugging. For example, consider the following action that logs the text of a line whenever it reaches it:

PS > cd c:\temp
PS > Set-PsBreakpoint .\looper.ps1 -line 3 -Action {
>>     $debugPreference = "Continue"
>>     Write-Debug (Get-Content .\looper.ps1)[2]
>> }
>>

  ID Script           Line Command         Variable        Action
  -- ------           ---- -------         --------        ------
   0 looper.ps1          3                                 ...


PS > .\looper.ps1
DEBUG:     "Count is: $count"
Count is: 0
DEBUG:     "Count is: $count"
Count is: 1
DEBUG:     "Count is: $count"
Count is: 2
DEBUG:     "Count is: $count"
(...)

When we create the breakpoint, we know which line we've set it on. When we hit the breakpoint, we can simply get the content of the script and return the appropriate line.

For an even more complete example of conditional breakpoints being used to perform code coverage analysis, see the section called “Program: Get Script Code Coverage”.

For more information about PowerShell's debugging support, Get-Help about_Debuggers.

Investigate System State While Debugging

Problem

PowerShell has paused execution after hitting a breakpoint, and you want to investigate the state of your script.

Solution

Examine the $PSDebugContext variable to investigate information about the current breakpoint and script location. Examine other variables to investigate the internal state of your script. Use the debug mode commands (Get-PsCallstack, List, and others) for more information about how you got to the current breakpoint, and what source code corresponds to the current location:

PS > Get-Content .\looper.ps1
param($userInput)

for($count = 0; $count -lt 10; $count++)
{
    "Count is: $count"
}

if($userInput -eq "One")
{
    "Got 'One'"
}

if($userInput -eq "Two")
{
    "Got 'Two'"
}

PS > Set-PsBreakpoint c:\temp\looper.ps1 -Line 5

  ID Script           Line Command         Variable        Action
  -- ------           ---- -------         --------        ------
   0 looper.ps1          5


PS > c:\temp\looper.ps1 -UserInput "Hello World"
Entering debug mode. Use h or ? for help.

Hit Line breakpoint on 'C:\temp\looper.ps1:5'

looper.ps1:5       "Count is: $count"
PS > $PSDebugContext.InvocationInfo.Line
    "Count is: $count"
PS > $PSDebugContext.InvocationInfo.ScriptLineNumber
5
PS > $count
0
PS > s
Count is: 0
looper.ps1:3   for($count = 0; $count -lt 10; $count++)
PS > s
looper.ps1:3   for($count = 0; $count -lt 10; $count++)
PS > s
Hit Line breakpoint on 'C:\temp\looper.ps1:5'

looper.ps1:5       "Count is: $count"
PS > s
Count is: 1
looper.ps1:3   for($count = 0; $count -lt 10; $count++)
PS > $count
1
PS > $userInput
Hello World
PS > Get-PsCallStack

Command                  Arguments                Location
-------                  ---------                --------
looper.ps1               {userInput=Hello World}  looper.ps1: Line 3
prompt                   {}                       prompt


PS > l 3 3

    3:* for($count = 0; $count -lt 10; $count++)
    4:  {
    5:      "Count is: $count"

PS > 

Discussion

When PowerShell pauses your script as it hits a breakpoint, it enters a debugging mode very much like the regular console session you are used to. You can execute commands, get and set variables, and otherwise explore the state of the system.

What makes debugging mode unique, however, is its context. When you enter commands in the PowerShell debugger, you are investigating the live state of the script. If you pause in the middle of a loop, you can view and modify the counter variable that controls that loop. Commands that you enter, in essence, become temporary parts of the script itself.

1 comment

  1. AndrewTearle Posted 16 days and 23 hours ago

    in essence

Add a comment

In addition to the regular variables available to you, PowerShell creates a new $PSDebugContext automatic variable whenever it reaches a breakpoint. The $PSDebugContext.BreakPoints property holds the current breakpoint, while the $PSDebugContext.InvocationInfo property holds information about the current location in the script:

PS > $PSDebugContext.InvocationInfo


MyCommand        :
BoundParameters  : {}
UnboundArguments : {}
ScriptLineNumber : 3
OffsetInLine     : 40
HistoryId        : -1
ScriptName       : C:\temp\looper.ps1
Line             : for($count = 0; $count -lt 10; $count++)
PositionMessage  :
                   At C:\temp\looper.ps1:3 char:40
                   + for($count = 0; $count -lt 10; $count++ <<<< )
InvocationName   : ++
PipelineLength   : 0
PipelinePosition : 0
ExpectingInput   : False
CommandOrigin    : Internal

For information about the nesting of functions and commands that called each other to reach this point (the "call stack,") type Get-PsCallStack.

If you find yourself continually monitoring a specific variable (or set of variables) for changes, the section called “Program: Watch an Expression for Changes” shows a script that lets you automatically watch an expression of your choice.

After investigating the state of the script, you can analyze its flow of execution through the three stepping commands: step into, step over, and step out. These functions single-step through your script with three different behaviors: entering functions and scripts as you go, skipping over functions and scripts as you go, or popping out of the current function or script (while still executing its remainder.)

For more information about PowerShell's debugging support, Get-Help about_Debuggers.

Program: Watch an Expression for Changes

When debugging a script (or even just generally using the shell), you might find yourself monitoring the same expression very frequently. This gets tedious to type by hand, so Example 14.6, “Watch-Expression.ps1” simplifies the task by automatically displaying the value of expressions that interest you as part of your prompt.

Example 14.6. Watch-Expression.ps1


<#

.SYNOPSIS

Updates your prompt to display the values of information you want to track. 


.EXAMPLE

PS >Watch-Expression { (Get-History).Count }

Expression          Value
----------          -----
(Get-History).Count     3

PS >Watch-Expression { $count }

Expression          Value
----------          -----
(Get-History).Count     4
$count

PS >$count = 100

Expression          Value
----------          -----
(Get-History).Count     5
$count                100

PS >Watch-Expression -Reset
PS >

#>

param(
    [ScriptBlock] $ScriptBlock,
    
    [Switch] $Reset
)

if($Reset)
{
    Remove-Item variable:\expressionWatch
    return
}

if(-not (Test-Path variable:\expressionWatch))
{
    $GLOBAL:expressionWatch = @()
}

$GLOBAL:expressionWatch += $scriptBlock

$oldPrompt = Get-Content function:\prompt
if($oldPrompt -notlike '*$expressionWatch*')
{
    $newPrompt = @'
        $results = foreach($expression in $expressionWatch)
        {
            New-Object PSObject -Property @{
                Expression = $expression.ToString().Trim();
                Value = & $expression
            } | Select Expression,Value
        }
        Write-Host "`n"
        Write-Host ($results | Format-Table -Auto | Out-String).Trim()
        Write-Host "`n"

'@

    $newPrompt += $oldPrompt

    Set-Item function:\prompt ([ScriptBlock]::Create($newPrompt))
}
      

For more information about running scripts, see the section called “Run Programs, Scripts, and Existing Tools”.

Program: Get Script Code Coverage

When developing a script, testing it (either automatically, or by hand) is a critical step in knowing how well it does the job you think it does. While you can spend enormous amounts of time testing new and interesting variations in your script, how do you know when you are done?

Code coverage is the standard technique to answer this question. You instrument your script so that the system knows what portions it executed, and then review the report at the end to see which portions were not executed. If a portion was not executed during your testing, you have untested code and can improve your confidence in its behavior by adding more tests.

In PowerShell, we can combine two powerful techniques to create a code coverage analysis tool: the tokenizer API, and conditional breakpoints.

First, we use the tokenizer API to discover all of the unique elements of our script: its statements, variables, loops, and more. Each token tells us the line and column that holds it, so we then create breakpoints for all of those line and column combinations.

When we hit a breakpoint, we record that we hit it, and continue.

Once the script completes, we can compare the entire set of tokens against the ones we actually hit. Any tokens that were not hit by a breakpoint represent gaps in our tests.

Example 14.7. Get-ScriptCoverage.ps1


<#

.SYNOPSIS

Uses conditional breakpoints to obtain information about what regions of
a script are executed when run.

.EXAMPLE

PS>$action = { c:\temp\looper.ps1 -UserInput 'One' }
PS>$coverage = Get-ScriptCoverage c:\temp\looper.ps1 -Action $action
PS>$coverage | Select Content,StartLine,StartColumn | Format-Table -Auto

Content   StartLine StartColumn
-------   --------- -----------
param     1         1
(         1         6
userInput 1         7
)         1         17
...       1         18
...       2         1
...       4         2
...       9         2
...       14        2
Got 'Two' 15        5
...       15        16
}         16        1
...       16        2

This example exercises a 'looper.ps1' script, and supplies it with some
user input. The output demonstrates that we didn't exercise the
"Got 'Two'" statement.

#>

param(
    $path,
    
    [ScriptBlock] $action = { & $path }
)

$scriptContent = Get-Content $path
$tokens = [System.Management.Automation.PsParser]::Tokenize(
    $scriptContent, [ref] $null)
$tokens = $tokens | Sort-Object StartLine,StartColumn

$GLOBAL:visitedTokens = @()

$breakpoints = foreach($token in $tokens)
{
    $breakAction = { $GLOBAL:hitTokens += $token }.GetNewClosure()
    
    Set-PsBreakpoint $path -Line `
        $token.StartLine -Column $token.StartColumn -Action $breakAction
}

. $action

$breakpoints | Remove-PsBreakpoint

$visitedTokens = $visitedTokens | Sort-Object -Unique StartLine,StartColumn
Compare-Object $tokens $visitedTokens -Property StartLine,StartColumn -PassThru

Remove-Item variable:\visitedTokens
      

2 comments

  1. Mike Martino Posted 5 days and 4 hours ago

    $hitTokens? Not that I'm affected by it, but $hit looks like an obfuscated curse word. Maybe that can be (un)intentional comedy. Just saying...

  2. Lee Holmes Posted 1 day and 15 hours ago

    Hah! Good one, thanks.

Add a comment


For more information about running scripts, see the section called “Run Programs, Scripts, and Existing Tools”.

You must sign in or register before commenting
*
*
*
*
*

Atom Icon Comments on this page or Comments on the whole book.