Add module to support Pester tests for automating debugger commands (stepInto, stepOut, etc.), along with basic tests (#9825)
This commit is contained in:
parent
af1de9e88d
commit
aac4c6ff21
|
@ -1237,9 +1237,8 @@ namespace System.Management.Automation
|
|||
|
||||
breakpoint.RemoveSelf(this);
|
||||
|
||||
if (_idToBreakpoint.Count == 0)
|
||||
if (CanDisableDebugger)
|
||||
{
|
||||
// The last breakpoint was removed, turn off debugging.
|
||||
SetInternalDebugMode(InternalDebugMode.Disabled);
|
||||
}
|
||||
|
||||
|
@ -2099,6 +2098,19 @@ namespace System.Management.Automation
|
|||
}
|
||||
}
|
||||
|
||||
private bool CanDisableDebugger
|
||||
{
|
||||
get
|
||||
{
|
||||
// The debugger can be disbled if there are no breakpoints
|
||||
// left and if we are not currently stepping in the debugger.
|
||||
return _idToBreakpoint.Count == 0 &&
|
||||
_currentDebuggerAction != DebuggerResumeAction.StepInto &&
|
||||
_currentDebuggerAction != DebuggerResumeAction.StepOver &&
|
||||
_currentDebuggerAction != DebuggerResumeAction.StepOut;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsSystemLockedDown
|
||||
{
|
||||
get
|
||||
|
@ -3857,9 +3869,8 @@ namespace System.Management.Automation
|
|||
_context.IgnoreScriptDebug = _savedIgnoreScriptDebug;
|
||||
_context.PSDebugTraceLevel = 0;
|
||||
_context.PSDebugTraceStep = false;
|
||||
if (!_idToBreakpoint.Any())
|
||||
if (CanDisableDebugger)
|
||||
{
|
||||
// Only disable debug mode if there are no breakpoints.
|
||||
SetInternalDebugMode(InternalDebugMode.Disabled);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -109,7 +109,7 @@ namespace System.Management.Automation
|
|||
fileContents: script);
|
||||
|
||||
internal static ScriptBlock CreateDelayParsedScriptBlock(string script, bool isProductCode)
|
||||
=> new ScriptBlock(new CompiledScriptBlockData(script, isProductCode));
|
||||
=> new ScriptBlock(new CompiledScriptBlockData(script, isProductCode)) { DebuggerHidden = true };
|
||||
|
||||
/// <summary>
|
||||
/// Returns a new scriptblock bound to a module. Any local variables in the
|
||||
|
|
|
@ -0,0 +1,442 @@
|
|||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the MIT License.
|
||||
|
||||
Describe 'Basic debugger command tests' -tag 'CI' {
|
||||
|
||||
BeforeAll {
|
||||
Register-DebuggerHandler
|
||||
}
|
||||
|
||||
AfterAll {
|
||||
Unregister-DebuggerHandler
|
||||
}
|
||||
|
||||
Context 'Help (?, h) command should display the debugger help message' {
|
||||
BeforeAll {
|
||||
$testScript = {
|
||||
try {
|
||||
$bp = Set-PSBreakpoint -Command Get-Process
|
||||
Get-Process -Id $PID > $null
|
||||
} finally {
|
||||
Remove-PSBreakPoint -Breakpoint $bp
|
||||
}
|
||||
}
|
||||
|
||||
$results = @(Test-Debugger -ScriptBlock $testScript -CommandQueue '?','h')
|
||||
$result = @{
|
||||
'?' = if ($results.Count -gt 0) {$results[0].Output -join [Environment]::NewLine}
|
||||
'h' = if ($results.Count -gt 1) {$results[1].Output -join [Environment]::NewLine}
|
||||
}
|
||||
}
|
||||
|
||||
It 'Should show 3 debugger commands were invoked' {
|
||||
# One extra for the implicit 'c' command that keeps the debugger automation moving
|
||||
$results.Count | Should -Be 3
|
||||
}
|
||||
|
||||
It '''h'' and ''?'' should show identical help messages' {
|
||||
$result['?'] | Should -BeExactly $result['h']
|
||||
}
|
||||
|
||||
It 'Should only have non-empty string output from the help command' {
|
||||
$results[0].Output | Should -BeOfType string
|
||||
$result['?'] | Should -Match '\S'
|
||||
}
|
||||
|
||||
It 'Should show help for stepInto' {$result['?'] | Should -Match '\ss, stepInto\s+'}
|
||||
It 'Should show help for stepOver' {$result['?'] | Should -Match '\sv, stepOver\s+'}
|
||||
It 'Should show help for stepOut' {$result['?'] | Should -Match '\so, stepOut\s+'}
|
||||
It 'Should show help for continue' {$result['?'] | Should -Match '\sc, continue\s+'}
|
||||
It 'Should show help for quit' {$result['?'] | Should -Match '\sq, quit\s+'}
|
||||
It 'Should show help for detach' {$result['?'] | Should -Match '\sd, detach\s+'}
|
||||
It 'Should show help for Get-PSCallStack' {$result['?'] | Should -Match '\sk, Get-PSCallStack\s+'}
|
||||
It 'Should show help for list' {$result['?'] | Should -Match '\sl, list\s+'}
|
||||
It 'Should show help for <enter>' {$result['?'] | Should -Match '\s<enter>\s+'}
|
||||
It 'Should show help for help' {$result['?'] | Should -Match '\s\?, h\s+'}
|
||||
}
|
||||
|
||||
Context 'List (l, list) command should show the script and the current position' {
|
||||
BeforeAll {
|
||||
$testScript = {
|
||||
try {
|
||||
$bp = Set-PSBreakpoint -Command Get-Process
|
||||
Get-Process -Id $PID > $null
|
||||
} finally {
|
||||
Remove-PSBreakPoint -Breakpoint $bp
|
||||
}
|
||||
}
|
||||
|
||||
$testScriptList = @'
|
||||
1:
|
||||
2: try {
|
||||
3: $bp = Set-PSBreakpoint -Command Get-Process
|
||||
4:* Get-Process -Id $PID > $null
|
||||
5: } finally {
|
||||
6: Remove-PSBreakPoint -Breakpoint $bp
|
||||
7: }
|
||||
8:
|
||||
'@
|
||||
|
||||
$results = @(Test-Debugger -ScriptBlock $testScript -CommandQueue 'l','list')
|
||||
$result = @{
|
||||
'l' = if ($results.Count -gt 0) {$results[0].Output -replace '\s+$' -join [Environment]::NewLine -replace "^[`r`n]+|[`r`n]+$"}
|
||||
'list' = if ($results.Count -gt 1) {$results[1].Output -replace '\s+$' -join [Environment]::NewLine -replace "^[`r`n]+|[`r`n]+$"}
|
||||
}
|
||||
}
|
||||
|
||||
It 'Should show 3 debugger commands were invoked' {
|
||||
# One extra for the implicit 'c' command that keeps the debugger automation moving
|
||||
$results.Count | Should -Be 3
|
||||
}
|
||||
|
||||
It '''l'' and ''list'' should show identical script listings' {
|
||||
$result['l'] | Should -BeExactly $result['list']
|
||||
}
|
||||
|
||||
It 'Should only have non-empty string output from the list command' {
|
||||
$results[0].Output | Should -BeOfType string
|
||||
$result['l'] | Should -Match '\S'
|
||||
}
|
||||
|
||||
It 'Should show the entire script listing with the current position on line 5' {
|
||||
$result['l'] | Should -BeExactly $testScriptList
|
||||
}
|
||||
}
|
||||
|
||||
Context 'List (l, list) command should support a start position' {
|
||||
BeforeAll {
|
||||
$testScript = {
|
||||
try {
|
||||
$bp = Set-PSBreakpoint -Command Get-Process
|
||||
Get-Process -Id $PID > $null
|
||||
} finally {
|
||||
Remove-PSBreakPoint -Breakpoint $bp
|
||||
}
|
||||
}
|
||||
|
||||
$testScriptList = @'
|
||||
4:* Get-Process -Id $PID > $null
|
||||
5: } finally {
|
||||
6: Remove-PSBreakPoint -Breakpoint $bp
|
||||
7: }
|
||||
8:
|
||||
'@
|
||||
|
||||
$results = @(Test-Debugger -ScriptBlock $testScript -CommandQueue 'l 4','list 4')
|
||||
$result = @{
|
||||
'l 4' = if ($results.Count -gt 0) {$results[0].Output -replace '\s+$' -join [Environment]::NewLine -replace "^[`r`n]+|[`r`n]+$"}
|
||||
'list 4' = if ($results.Count -gt 1) {$results[1].Output -replace '\s+$' -join [Environment]::NewLine -replace "^[`r`n]+|[`r`n]+$"}
|
||||
}
|
||||
}
|
||||
|
||||
It 'Should show 3 debugger commands were invoked' {
|
||||
# One extra for the implicit 'c' command that keeps the debugger automation moving
|
||||
$results.Count | Should -Be 3
|
||||
}
|
||||
|
||||
It '''l 4'' and ''list 4'' should show identical script listings' {
|
||||
$result['l 4'] | Should -BeExactly $result['list 4']
|
||||
}
|
||||
|
||||
It 'Should only have non-empty string output from the list command' {
|
||||
$results[0].Output | Should -BeOfType string
|
||||
$result['l 4'] | Should -Match '\S'
|
||||
}
|
||||
|
||||
It 'Should show a partial script listing starting on line 4 with the current position on line 5' {
|
||||
$result['l 4'] | Should -BeExactly $testScriptList
|
||||
}
|
||||
}
|
||||
|
||||
Context 'List (l, list) command should support a start position and a line count' {
|
||||
BeforeAll {
|
||||
$testScript = {
|
||||
try {
|
||||
$bp = Set-PSBreakpoint -Command Get-Process
|
||||
Get-Process -Id $PID > $null
|
||||
} finally {
|
||||
Remove-PSBreakPoint -Breakpoint $bp
|
||||
}
|
||||
}
|
||||
|
||||
$testScriptList = @'
|
||||
3: $bp = Set-PSBreakpoint -Command Get-Process
|
||||
4:* Get-Process -Id $PID > $null
|
||||
'@
|
||||
|
||||
$results = @(Test-Debugger -ScriptBlock $testScript -CommandQueue 'l 3 2','list 3 2')
|
||||
$result = @{
|
||||
'l 3 2' = if ($results.Count -gt 0) {$results[0].Output -replace '\s+$' -join [Environment]::NewLine -replace "^[`r`n]+|[`r`n]+$"}
|
||||
'list 3 2' = if ($results.Count -gt 1) {$results[1].Output -replace '\s+$' -join [Environment]::NewLine -replace "^[`r`n]+|[`r`n]+$"}
|
||||
}
|
||||
}
|
||||
|
||||
It 'Should show 3 debugger commands were invoked' {
|
||||
# One extra for the implicit 'c' command that keeps the debugger automation moving
|
||||
$results.Count | Should -Be 3
|
||||
}
|
||||
|
||||
It '''l 3 2'' and ''list 3 2'' should show identical script listings' {
|
||||
$result['l 3 2'] | Should -BeExactly $result['list 3 2']
|
||||
}
|
||||
|
||||
It 'Should only have non-empty string output from the list command' {
|
||||
$results[0].Output | Should -BeOfType string
|
||||
$result['l 3 2'] | Should -Match '\S'
|
||||
}
|
||||
|
||||
It 'Should show a partial script listing showing 3 lines starting on line 4 with the current position on line 5' {
|
||||
$result['l 3 2'] | Should -BeExactly $testScriptList
|
||||
}
|
||||
}
|
||||
|
||||
Context 'Callstack (k, Get-PSCallStack) command should show the current call stack' {
|
||||
BeforeAll {
|
||||
$testScript = {
|
||||
try {
|
||||
$bp = Set-PSBreakpoint -Command Get-Process
|
||||
Get-Process -Id $PID > $null
|
||||
} finally {
|
||||
Remove-PSBreakPoint -Breakpoint $bp
|
||||
}
|
||||
}
|
||||
|
||||
$results = @(Test-Debugger -ScriptBlock $testScript -CommandQueue 'k','Get-PSCallStack')
|
||||
$result = @{
|
||||
'k' = if ($results.Count -gt 0) {$results[0].Output}
|
||||
'Get-PSCallStack' = if ($results.Count -gt 1) {$results[1].Output}
|
||||
}
|
||||
}
|
||||
|
||||
It 'Should show 3 debugger commands were invoked' {
|
||||
# One extra for the implicit 'c' command that keeps the debugger automation moving
|
||||
$results.Count | Should -Be 3
|
||||
}
|
||||
|
||||
It 'Should only have CallStackFrame output from the callstack command' {
|
||||
$results[0].Output | Should -BeOfType System.Management.Automation.CallStackFrame
|
||||
}
|
||||
|
||||
It '''k'' and ''Get-PSCallStack'' should show identical script listings' {
|
||||
[string[]]$result['k'] -join [Environment]::NewLine | Should -BeExactly ([string[]]$result['Get-PSCallStack'] -join [Environment]::NewLine)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Describe 'Simple debugger stepping command tests' -tag 'CI' {
|
||||
|
||||
BeforeAll {
|
||||
Register-DebuggerHandler
|
||||
}
|
||||
|
||||
AfterAll {
|
||||
Unregister-DebuggerHandler
|
||||
}
|
||||
|
||||
Context 'StepInto steps into the current command if possible; otherwise it steps over the command' {
|
||||
BeforeAll {
|
||||
$testScript = {
|
||||
try {
|
||||
$bp = Set-PSBreakpoint -Command ForEach-Object
|
||||
Get-Process -Id $PID | ForEach-Object {
|
||||
'One fish, two fish'
|
||||
'Red fish, blue fish'
|
||||
} *> $null
|
||||
} finally {
|
||||
Remove-PSBreakPoint -Breakpoint $bp
|
||||
}
|
||||
}
|
||||
|
||||
$result = @{
|
||||
's' = @(Test-Debugger -ScriptBlock $testScript -CommandQueue 's','s','s','s')
|
||||
'stepInto' = @(Test-Debugger -ScriptBlock $testScript -CommandQueue 'stepInto','stepInto','stepInto','stepInto')
|
||||
}
|
||||
}
|
||||
|
||||
It 'Should show 4 debugger commands were invoked twice' {
|
||||
# One extra for the implicit 'c' command that keeps the debugger automation moving
|
||||
$result['s'].Count | Should -Be 5
|
||||
$result['stepInto'].Count | Should -Be 5
|
||||
}
|
||||
|
||||
It '''s'' and ''stepInto'' should have identical behaviour' {
|
||||
for ($i = 0; $i -lt 3; $i++) {
|
||||
$result['s'][$i] | ShouldHaveSameExtentAs -DebuggerCommandResult $result['stepInto'][$i]
|
||||
}
|
||||
}
|
||||
|
||||
It 'The first extent should be the statement containing ForEach-Object' {
|
||||
$result['s'][0] | ShouldHaveExtent -FromLine 4 -FromColumn 21 -ToLine 7 -ToColumn 31
|
||||
}
|
||||
|
||||
It 'The second extent should be in the nested scriptblock' {
|
||||
$result['s'][1] | ShouldHaveExtent -Line 4 -FromColumn 59 -ToColumn 60
|
||||
}
|
||||
|
||||
It 'The third extent should be on ''One fish, two fish''' {
|
||||
$result['s'][2] | ShouldHaveExtent -Line 5 -FromColumn 25 -ToColumn 45
|
||||
}
|
||||
|
||||
It 'The fourth extent should be on ''Red fish, blue fish''' {
|
||||
$result['s'][3] | ShouldHaveExtent -Line 6 -FromColumn 25 -ToColumn 46
|
||||
}
|
||||
}
|
||||
|
||||
Context 'StepOver steps over the current command, unless it contains a triggerable breakpoint' {
|
||||
BeforeAll {
|
||||
$testScript = {
|
||||
try {
|
||||
$bp1 = Set-PSBreakpoint -Command ForEach-Object
|
||||
$bp2 = Set-PSBreakpoint -Command ConvertTo-Csv | Disable-PSBreakpoint -PassThru
|
||||
Get-Process -Id $PID | ForEach-Object -Process {
|
||||
$_ | ConvertTo-Csv
|
||||
} *> $null
|
||||
Enable-PSBreakpoint -Breakpoint $bp2
|
||||
& {
|
||||
Get-Date | ConvertTo-Csv
|
||||
} *> $null
|
||||
} finally {
|
||||
Remove-PSBreakPoint -Breakpoint $bp1,$bp2
|
||||
}
|
||||
}
|
||||
|
||||
$result = @{
|
||||
'v' = @(Test-Debugger -ScriptBlock $testScript -CommandQueue 'v','v','v','v')
|
||||
'stepOver' = @(Test-Debugger -ScriptBlock $testScript -CommandQueue 'stepOver','stepOver','stepOver','stepOver')
|
||||
}
|
||||
}
|
||||
|
||||
It 'Should show 4 debugger commands were invoked twice' {
|
||||
# One extra for the implicit 'c' command that keeps the debugger automation moving
|
||||
$result['v'].Count | Should -Be 5
|
||||
$result['stepOver'].Count | Should -Be 5
|
||||
}
|
||||
|
||||
It '''v'' and ''stepOver'' should have identical behaviour' {
|
||||
for ($i = 0; $i -lt 3; $i++) {
|
||||
$result['v'][$i] | ShouldHaveSameExtentAs -DebuggerCommandResult $result['stepOver'][$i]
|
||||
}
|
||||
}
|
||||
|
||||
It 'The first extent should be the statement containing ForEach-Object' {
|
||||
$result['v'][0] | ShouldHaveExtent -FromLine 5 -FromColumn 21 -ToLine 7 -ToColumn 31
|
||||
}
|
||||
|
||||
It 'The second extent should be on Enable-PSBreakpoint' {
|
||||
$result['v'][1] | ShouldHaveExtent -Line 8 -FromColumn 21 -ToColumn 57
|
||||
}
|
||||
|
||||
It 'The third extent should be on the script block invoked with the call operator' {
|
||||
$result['v'][2] | ShouldHaveExtent -FromLine 9 -FromColumn 21 -ToLine 11 -ToColumn 31
|
||||
}
|
||||
|
||||
It 'The fourth extent should be on the ConvertTo-Csv breakpoint inside the script block' {
|
||||
$result['v'][3] | ShouldHaveExtent -Line 10 -FromColumn 25 -ToColumn 49
|
||||
}
|
||||
}
|
||||
|
||||
Context 'StepOut steps out of the current command, unless it contains a triggerable breakpoint after the current location' {
|
||||
BeforeAll {
|
||||
$testScript = {
|
||||
try {
|
||||
$bps = Set-PSBreakpoint -Command Get-Process,ConvertTo-Csv
|
||||
& {
|
||||
$process = Get-Process -Id $PID
|
||||
$process.Id
|
||||
}
|
||||
$date = Get-Date
|
||||
$date | ConvertTo-Csv
|
||||
} finally {
|
||||
Remove-PSBreakPoint -Breakpoint $bps
|
||||
}
|
||||
}
|
||||
|
||||
$result = @{
|
||||
'o' = @(Test-Debugger -ScriptBlock $testScript -CommandQueue 'o','o','o')
|
||||
'stepOut' = @(Test-Debugger -ScriptBlock $testScript -CommandQueue 'stepOut','stepOut','stepOut')
|
||||
}
|
||||
}
|
||||
|
||||
It 'Should show 3 debugger commands were invoked twice' {
|
||||
# One extra for the implicit 'c' command that keeps the debugger automation moving
|
||||
$result['o'].Count | Should -Be 4
|
||||
$result['stepOut'].Count | Should -Be 4
|
||||
}
|
||||
|
||||
It '''o'' and ''stepOut'' should have identical behaviour' {
|
||||
for ($i = 0; $i -lt 3; $i++) {
|
||||
$result['o'][$i] | ShouldHaveSameExtentAs -DebuggerCommandResult $result['stepOut'][$i]
|
||||
}
|
||||
}
|
||||
|
||||
It 'The first extent should be on Get-Process' {
|
||||
$result['o'][0] | ShouldHaveExtent -Line 5 -FromColumn 25 -ToColumn 56
|
||||
}
|
||||
|
||||
It 'The second extent should be on Get-Date' {
|
||||
$result['o'][1] | ShouldHaveExtent -Line 8 -FromColumn 21 -ToColumn 37
|
||||
}
|
||||
|
||||
It 'The third extent should be on the ConvertTo-Csv breakpoint' {
|
||||
$result['o'][2] | ShouldHaveExtent -Line 9 -FromColumn 21 -ToColumn 42
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Describe 'Debugger bug fix tests' -tag 'CI' {
|
||||
|
||||
BeforeAll {
|
||||
Register-DebuggerHandler
|
||||
}
|
||||
|
||||
AfterAll {
|
||||
Unregister-DebuggerHandler
|
||||
}
|
||||
|
||||
Context 'Stepping works beyond Remove-PSBreakpoint (Issue #9824)' {
|
||||
BeforeAll {
|
||||
$testScript = {
|
||||
function Test-Issue9824 {
|
||||
$bp = Set-PSBreakpoint -Command Remove-PSBreakpoint
|
||||
Remove-PSBreakPoint -Breakpoint $bp
|
||||
}
|
||||
Test-Issue9824
|
||||
1 + 1
|
||||
}
|
||||
|
||||
$result = @{
|
||||
's' = @(Test-Debugger -ScriptBlock $testScript -CommandQueue 's','s','s')
|
||||
'v' = @(Test-Debugger -ScriptBlock $testScript -CommandQueue 'v','v','v')
|
||||
'o' = @(Test-Debugger -ScriptBlock $testScript -CommandQueue 'o','o')
|
||||
}
|
||||
}
|
||||
|
||||
It 'Should show 3 debugger commands were invoked for stepInto' {
|
||||
# One extra for the implicit 'c' command that keeps the debugger automation moving
|
||||
$result['s'].Count | Should -Be 4
|
||||
}
|
||||
|
||||
It 'Should show 3 debugger commands were invoked for stepOver' {
|
||||
# One extra for the implicit 'c' command that keeps the debugger automation moving
|
||||
$result['v'].Count | Should -Be 4
|
||||
}
|
||||
|
||||
It 'Should show 2 debugger commands were invoked for stepOut' {
|
||||
# One extra for the implicit 'c' command that keeps the debugger automation moving
|
||||
$result['o'].Count | Should -Be 3
|
||||
}
|
||||
|
||||
It 'The last extent for stepInto should be on 1 + 1' {
|
||||
$result['s'][2] | ShouldHaveExtent -Line 7 -FromColumn 17 -ToColumn 22
|
||||
}
|
||||
|
||||
It 'The last extent for stepOver should be on 1 + 1' {
|
||||
$result['v'][2] | ShouldHaveExtent -Line 7 -FromColumn 17 -ToColumn 22
|
||||
}
|
||||
|
||||
It 'The last extent for stepOut should be on 1 + 1' {
|
||||
$result['o'][1] | ShouldHaveExtent -Line 7 -FromColumn 17 -ToColumn 22
|
||||
}
|
||||
}
|
||||
}
|
30
test/tools/Modules/HelpersDebugger/HelpersDebugger.psd1
Normal file
30
test/tools/Modules/HelpersDebugger/HelpersDebugger.psd1
Normal file
|
@ -0,0 +1,30 @@
|
|||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the MIT License.
|
||||
|
||||
@{
|
||||
RootModule = 'HelpersDebugger.psm1'
|
||||
|
||||
ModuleVersion = '1.0'
|
||||
|
||||
GUID = '37a454d7-8acd-40e6-8a2c-43c9d46b1b0c'
|
||||
|
||||
CompanyName = 'Microsoft Corporation'
|
||||
|
||||
Copyright = 'Copyright (c) Microsoft Corporation. All rights reserved.'
|
||||
|
||||
Description = 'Helper module for Pester tests that automate the debugger'
|
||||
|
||||
PowerShellVersion = '5.0'
|
||||
|
||||
FunctionsToExport = @(
|
||||
'Register-DebuggerHandler'
|
||||
'ShouldHaveExtent'
|
||||
'ShouldHaveSameExtentAs'
|
||||
'Test-Debugger'
|
||||
'Unregister-DebuggerHandler'
|
||||
)
|
||||
|
||||
CmdletsToExport = @()
|
||||
|
||||
AliasesToExport = @()
|
||||
}
|
219
test/tools/Modules/HelpersDebugger/HelpersDebugger.psm1
Normal file
219
test/tools/Modules/HelpersDebugger/HelpersDebugger.psm1
Normal file
|
@ -0,0 +1,219 @@
|
|||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the MIT License.
|
||||
|
||||
# Ensure that terminating errors terminate when importing the module.
|
||||
trap {throw $_}
|
||||
|
||||
# Strict mode FTW.
|
||||
Set-StrictMode -Version Latest
|
||||
|
||||
# Enable explicit export so that there are no surprises with commands exported from the module.
|
||||
Export-ModuleMember
|
||||
|
||||
# Grab the internal ScriptPosition property once and re-use it in the ps1xml file
|
||||
$internalExtentProperty = [System.Management.Automation.InvocationInfo].GetProperty('ScriptPosition', [System.Reflection.BindingFlags]'NonPublic,Instance')
|
||||
|
||||
# A debugger handler that can be used to automatically control the debugger
|
||||
$debuggerStopHandler = {
|
||||
param($s, $e)
|
||||
# If we're not handling a debugger stop event during the execution of
|
||||
# Test-Debugger, then simply continue execution
|
||||
if (@(Get-Variable -Name dbgCmdQueue,dbgResults -Scope Script -ErrorAction Ignore).Count -ne 2) {
|
||||
$e.ResumeAction = [System.Management.Automation.DebuggerResumeAction]::Continue
|
||||
return
|
||||
}
|
||||
do {
|
||||
if ($script:dbgCmdQueue.Count -eq 0) {
|
||||
# If there are no more commands to process, continue execution
|
||||
$stringDbgCommand = 'c'
|
||||
} else {
|
||||
$stringDbgCommand = $script:dbgCmdQueue.Dequeue()
|
||||
}
|
||||
$dbgCmd = [System.Management.Automation.PSCommand]::new()
|
||||
$dbgCmd.AddCommand($stringDbgCommand)
|
||||
$output = [System.Management.Automation.PSDataCollection[PSObject]]::new()
|
||||
$result = $Host.Runspace.Debugger.ProcessCommand($dbgCmd, $output)
|
||||
$script:dbgResults += [pscustomobject]@{
|
||||
PSTypeName = 'DebuggerCommandResult'
|
||||
Command = $stringDbgCommand
|
||||
Context = $PSDebugContext
|
||||
Output = $output
|
||||
}
|
||||
} while ($result -eq $null -or $result.ResumeAction -eq $null)
|
||||
$e.ResumeAction = $result.ResumeAction
|
||||
}
|
||||
|
||||
# A flag to identify if the debugger handler has been added or not
|
||||
$debuggerStopHandlerRegistered = $false
|
||||
|
||||
function Register-DebuggerHandler {
|
||||
[CmdletBinding()]
|
||||
[OutputType([System.Void])]
|
||||
param()
|
||||
try {
|
||||
$callerEAP = $ErrorActionPreference
|
||||
# We disable debugger interactivity so that all debugger events go through
|
||||
# the DebuggerStop event only (i.e. breakpoints don't actually generate a
|
||||
# prompt for user interaction)
|
||||
$host.DebuggerEnabled = $false
|
||||
$host.Runspace.Debugger.add_DebuggerStop($script:debuggerStopHandler)
|
||||
$script:debuggerStopHandlerRegistered = $true
|
||||
} catch {
|
||||
Write-Error -ErrorRecord $_ -ErrorAction $callerEAP
|
||||
}
|
||||
}
|
||||
Export-ModuleMember -Function Register-DebuggerHandler
|
||||
|
||||
function Unregister-DebuggerHandler {
|
||||
[CmdletBinding()]
|
||||
[OutputType([System.Void])]
|
||||
param()
|
||||
try {
|
||||
$callerEAP = $ErrorActionPreference
|
||||
$host.Runspace.Debugger.remove_DebuggerStop($script:debuggerStopHandler)
|
||||
$host.DebuggerEnabled = $true
|
||||
$script:debuggerStopHandlerRegistered = $false
|
||||
} catch {
|
||||
Write-Error -ErrorRecord $_ -ErrorAction $callerEAP
|
||||
}
|
||||
}
|
||||
Export-ModuleMember -Function Unregister-DebuggerHandler
|
||||
|
||||
function Test-Debugger {
|
||||
[CmdletBinding()]
|
||||
[OutputType('DebuggerCommandResult')]
|
||||
param(
|
||||
[Parameter(Position=0, Mandatory)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[Alias('sb')]
|
||||
[ScriptBlock]
|
||||
$ScriptBlock,
|
||||
|
||||
[Parameter()]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[string[]]
|
||||
$CommandQueue
|
||||
)
|
||||
try {
|
||||
$callerEAP = $ErrorActionPreference
|
||||
# If the debugger is not set up properly, notify the user with an error message
|
||||
if (-not $script:debuggerStopHandlerRegistered -or $host.DebuggerEnabled) {
|
||||
$message = 'You must invoke Register-DebuggerHandler before invoking Test-Debugger, and Unregister-DebuggerHandler after invoking Test-Debugger. As a best practice, invoke Register-DebuggerHandler in the BeforeAll block and Unregister-DebuggerHandler in the AfterAll block of your test script.'
|
||||
$exception = [System.InvalidOperationException]::new($message)
|
||||
$errorRecord = [System.Management.Automation.ErrorRecord]::new($exception, $exception.GetType().Name, 'InvalidOperation', $null)
|
||||
throw $errorRecord
|
||||
}
|
||||
$script:dbgResults = @()
|
||||
$script:dbgCmdQueue = [System.Collections.Queue]::new()
|
||||
foreach ($command in $CommandQueue) {
|
||||
$script:dbgCmdQueue.Enqueue($command)
|
||||
}
|
||||
# We re-create the script block before invoking it to ensure that it will
|
||||
# work regardless of where the script itself was defined in the test file.
|
||||
# We also silence any standard output because this invocation is about the
|
||||
# debugger output, not the output of the script itself.
|
||||
& {
|
||||
[System.Diagnostics.DebuggerStepThrough()]
|
||||
[CmdletBinding()]
|
||||
param()
|
||||
try {
|
||||
$ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop
|
||||
[ScriptBlock]::Create($ScriptBlock).Invoke() > $null
|
||||
} catch {
|
||||
Write-Error -ErrorRecord $_ -ErrorAction Stop
|
||||
}
|
||||
}
|
||||
$script:dbgResults
|
||||
} catch {
|
||||
Write-Error -ErrorRecord $_ -ErrorAction $callerEAP
|
||||
} finally {
|
||||
Remove-Variable -Name dbgResults -Scope Script -ErrorAction Ignore
|
||||
Remove-Variable -Name dbgCmdQueue -Scope Script -ErrorAction Ignore
|
||||
}
|
||||
}
|
||||
Export-ModuleMember -Function Test-Debugger
|
||||
|
||||
function Get-DebuggerExtent {
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Position=0, Mandatory, ValueFromPipeline)]
|
||||
[ValidateNotNull()]
|
||||
[PSTypeName('DebuggerCommandResult')]
|
||||
$DebuggerCommandResult
|
||||
)
|
||||
process {
|
||||
try {
|
||||
$callerEAP = $ErrorActionPreference
|
||||
$script:internalExtentProperty.GetValue($DebuggerCommandResult.Context.InvocationInfo)
|
||||
} catch {
|
||||
Write-Error -ErrorRecord $_ -ErrorAction $callerEAP
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function ShouldHaveExtent {
|
||||
[CmdletBinding(DefaultParameterSetName='SingleLineExtent')]
|
||||
param(
|
||||
[Parameter(Position=0, Mandatory, ValueFromPipeline)]
|
||||
[ValidateNotNull()]
|
||||
[PSTypeName('DebuggerCommandResult')]
|
||||
$DebuggerCommandResult,
|
||||
|
||||
[Parameter(Mandatory, ParameterSetName='SingleLineExtent')]
|
||||
[ValidateRange(1, [int]::MaxValue)]
|
||||
[int]
|
||||
$Line,
|
||||
|
||||
[Parameter(Mandatory, ParameterSetName='MultilineExtent')]
|
||||
[ValidateRange(1, [int]::MaxValue)]
|
||||
[int]
|
||||
$FromLine,
|
||||
|
||||
[Parameter(Mandatory)]
|
||||
[ValidateRange(1, [int]::MaxValue)]
|
||||
[int]
|
||||
$FromColumn,
|
||||
|
||||
[Parameter(Mandatory, ParameterSetName='MultilineExtent')]
|
||||
[ValidateRange(1, [int]::MaxValue)]
|
||||
[int]
|
||||
$ToLine,
|
||||
|
||||
[Parameter(Mandatory)]
|
||||
[ValidateRange(1, [int]::MaxValue)]
|
||||
[int]
|
||||
$ToColumn
|
||||
)
|
||||
process {
|
||||
$extent = Get-DebuggerExtent -DebuggerCommandResult $DebuggerCommandResult
|
||||
$extent.StartLineNumber | Should -Be $(if ($PSCmdlet.ParameterSetName -eq 'SingleLineExtent') {$Line} else {$FromLine})
|
||||
$extent.StartColumnNumber | Should -Be $FromColumn
|
||||
$extent.EndLineNumber | Should -Be $(if ($PSCmdlet.ParameterSetName -eq 'SingleLineExtent') {$Line} else {$ToLine})
|
||||
$extent.EndColumnNumber | Should -Be $ToColumn
|
||||
}
|
||||
}
|
||||
Export-ModuleMember -Function ShouldHaveExtent
|
||||
|
||||
function ShouldHaveSameExtentAs {
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Position=0, Mandatory, ValueFromPipeline)]
|
||||
[ValidateNotNull()]
|
||||
[PSTypeName('DebuggerCommandResult')]
|
||||
$SourceDebuggerCommandResult,
|
||||
|
||||
[Parameter(Position=1, Mandatory)]
|
||||
[ValidateNotNull()]
|
||||
[Alias('DebuggerCommandResult')]
|
||||
[PSTypeName('DebuggerCommandResult')]
|
||||
$TargetDebuggerCommandResult
|
||||
)
|
||||
begin {
|
||||
$targetExtent = Get-DebuggerExtent -DebuggerCommandResult $TargetDebuggerCommandResult
|
||||
}
|
||||
process {
|
||||
$sourceExtent = Get-DebuggerExtent -DebuggerCommandResult $SourceDebuggerCommandResult
|
||||
$sourceExtent | Should -Be $targetExtent
|
||||
}
|
||||
}
|
||||
Export-ModuleMember -Function ShouldHaveSameExtentAs
|
Loading…
Reference in a new issue