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
5 changed files with 707 additions and 5 deletions
|
@ -1237,9 +1237,8 @@ namespace System.Management.Automation
|
||||||
|
|
||||||
breakpoint.RemoveSelf(this);
|
breakpoint.RemoveSelf(this);
|
||||||
|
|
||||||
if (_idToBreakpoint.Count == 0)
|
if (CanDisableDebugger)
|
||||||
{
|
{
|
||||||
// The last breakpoint was removed, turn off debugging.
|
|
||||||
SetInternalDebugMode(InternalDebugMode.Disabled);
|
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
|
private static bool IsSystemLockedDown
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
|
@ -3857,9 +3869,8 @@ namespace System.Management.Automation
|
||||||
_context.IgnoreScriptDebug = _savedIgnoreScriptDebug;
|
_context.IgnoreScriptDebug = _savedIgnoreScriptDebug;
|
||||||
_context.PSDebugTraceLevel = 0;
|
_context.PSDebugTraceLevel = 0;
|
||||||
_context.PSDebugTraceStep = false;
|
_context.PSDebugTraceStep = false;
|
||||||
if (!_idToBreakpoint.Any())
|
if (CanDisableDebugger)
|
||||||
{
|
{
|
||||||
// Only disable debug mode if there are no breakpoints.
|
|
||||||
SetInternalDebugMode(InternalDebugMode.Disabled);
|
SetInternalDebugMode(InternalDebugMode.Disabled);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -109,7 +109,7 @@ namespace System.Management.Automation
|
||||||
fileContents: script);
|
fileContents: script);
|
||||||
|
|
||||||
internal static ScriptBlock CreateDelayParsedScriptBlock(string script, bool isProductCode)
|
internal static ScriptBlock CreateDelayParsedScriptBlock(string script, bool isProductCode)
|
||||||
=> new ScriptBlock(new CompiledScriptBlockData(script, isProductCode));
|
=> new ScriptBlock(new CompiledScriptBlockData(script, isProductCode)) { DebuggerHidden = true };
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns a new scriptblock bound to a module. Any local variables in the
|
/// 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