From 395a4488300e59ab3144bbbc7a5a6f2e6ca77ad8 Mon Sep 17 00:00:00 2001 From: Kirk Munro Date: Mon, 3 Jun 2019 23:40:51 -0300 Subject: [PATCH 1/7] add HelpersDebugger module and debugger tests --- .../Debugging/DebuggerCommand.Tests.ps1 | 325 ++++++++++++++++++ .../HelpersDebugger/HelpersDebugger.psd1 | 28 ++ .../HelpersDebugger/HelpersDebugger.psm1 | 185 ++++++++++ 3 files changed, 538 insertions(+) create mode 100644 test/powershell/Language/Scripting/Debugging/DebuggerCommand.Tests.ps1 create mode 100644 test/tools/Modules/HelpersDebugger/HelpersDebugger.psd1 create mode 100644 test/tools/Modules/HelpersDebugger/HelpersDebugger.psm1 diff --git a/test/powershell/Language/Scripting/Debugging/DebuggerCommand.Tests.ps1 b/test/powershell/Language/Scripting/Debugging/DebuggerCommand.Tests.ps1 new file mode 100644 index 00000000000..252323a42e2 --- /dev/null +++ b/test/powershell/Language/Scripting/Debugging/DebuggerCommand.Tests.ps1 @@ -0,0 +1,325 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +Describe 'Simple 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} + $resulth = 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 'Should only have non-empty string output from the help command' { + $results[0].Output | Should -BeOfType string + $result? | Should -Match '\S' + } + + It '''h'' and ''?'' should show identical help messages' { + $result? | Should -BeExactly $resulth + } + + 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 ' {$result? | Should -Match '\s\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: & { + 5:* Get-Process -Id $PID + 6: } > $null + 7: } finally { + 8: Remove-PSBreakPoint -Breakpoint $bp + 9: } + 10: +'@ + + $results = @(Test-Debugger -ScriptBlock $testScript -CommandQueue 'l','list') + $resultl = if ($results.Count -gt 0) {$results[0].Output -replace '\s+$' -join [Environment]::NewLine -replace "^[`r`n]+|[`r`n]+$"} + $resultlist = 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 'Should only have non-empty string output from the list command' { + $results[0].Output | Should -BeOfType string + $resultl | Should -Match '\S' + } + + It '''l'' and ''list'' should show identical script listings' { + $resultl | Should -BeExactly $resultlist + } + + It 'Should show the entire script listing with the current position on line 5' { + $resultl | 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') + $resultk = if ($results.Count -gt 0) {$results[0].Output} + $resultgcs = 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[]]$resultk -join [Environment]::NewLine | Should -BeExactly ([string[]]$resultgcs -join [Environment]::NewLine) + } + } + +} + +Describe '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 + $sb = { + 'One fish, two fish' + 'Red fish, blue fish' + } + & { + Get-Process -Id $PID | ForEach-Object -Process $sb + } *> $null + } finally { + Remove-PSBreakPoint -Breakpoint $bp + } + } + + $results = @(Test-Debugger -ScriptBlock $testScript -CommandQueue 's','s','s','s') + $resultstepinto = @(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 + $results.Count | Should -Be 5 + $resultstepinto.Count | Should -Be 5 + } + + It '''s'' and ''stepInto'' should have identical behaviour' { + for ($i = 0; $i -lt 3; $i++) { + Get-DebuggerExtent -DebuggerCommandResult $results[$i] | Should -Be (Get-DebuggerExtent -DebuggerCommandResult $resultstepinto[$i]) + } + } + + It 'The first extent should be the statement containing ForEach-Object' { + $results[0] | ShouldHaveExtent -Line 9 -FromColumn 25 -ToColumn 75 + } + + It 'The second extent should be in the nested scriptblock' { + $results[1] | ShouldHaveExtent -Line 4 -FromColumn 27 -ToColumn 28 + } + + It 'The third extent should be on Write-Object' { + $results[2] | ShouldHaveExtent -Line 5 -FromColumn 25 -ToColumn 45 + } + + It 'The fourth extent should be on ''Hello''' { + $results[3] | ShouldHaveExtent -Line 6 -FromColumn 25 -ToColumn 46 + } + } +} + + <# + It '-ErrorAction Break enters the debugger on a terminating error' { + $testScript = { + & { + [CmdletBinding()] + param() + 'Hello' + Get-Process -Id ([int]::MaxValue + 1) + 'Goodbye' + } -ErrorAction Break + } + + $results = Test-Debugger -ScriptBlock $testScript + $results.Count | Should -Be 1 + $results[0] | ShouldHaveExtent -Line 6 -FromColumn 21 -ToColumn 58 + } + + It '-ErrorAction Break does NOT enter the debugger on a naked rethrow' { + $testScript = { + & { + [CmdletBinding()] + param() + try { + 'Hello' + Get-Process -Id ([int]::MaxValue) -ErrorAction Stop + 'Goodbye' + } catch { + throw + } + } -ErrorAction Break + } + + $results = Test-Debugger -ScriptBlock $testScript + $results.Count | Should -Be 1 + $results[0] | ShouldHaveExtent -Line 7 -FromColumn 25 -ToColumn 76 + } + + It '-ErrorAction Break does enter the debugger on a new throw' { + $testScript = { + & { + [CmdletBinding()] + param() + try { + 'Hello' + Get-Process -Id ([int]::MaxValue) -ErrorAction Stop + 'Goodbye' + } catch { + throw $_ + } + } -ErrorAction Break + } + + $results = Test-Debugger -ScriptBlock $testScript + $results.Count | Should -Be 2 + $results[0] | ShouldHaveExtent -Line 7 -FromColumn 25 -ToColumn 76 + $results[1] | ShouldHaveExtent -Line 10 -FromColumn 25 -ToColumn 33 + } + + It '-ErrorAction Break does NOT enter the debugger for errors inside a DebuggerHidden block' { + $testScript = { + & { + [System.Diagnostics.DebuggerHidden()] + [CmdletBinding()] + param() + 1/0 # We shouldn't be able to break here with -ErrorAction Break + } -ErrorAction Break + } + + $results = Test-Debugger -ScriptBlock $testScript + $results.Count | Should -Be 0 + } + + It '-ErrorAction Break does NOT enter the debugger for errors in a nested function context under a DebuggerHidden block' { + $testScript = { + & { + [System.Diagnostics.DebuggerHidden()] + [CmdletBinding()] + param() + & { + [CmdletBinding()] + param() + 1/0 # We shouldn't be able to break here with -ErrorAction Break + } + } -ErrorAction Break + } + + $results = Test-Debugger -ScriptBlock $testScript + $results.Count | Should -Be 0 + } + + It '-ErrorAction Break does NOT enter the debugger for errors inside a DebuggerStepThrough block' { + $testScript = { + & { + [System.Diagnostics.DebuggerStepThrough()] + [CmdletBinding()] + param() + 1/0 # We shouldn't be able to break here with -ErrorAction Break + } -ErrorAction Break + } + + $results = Test-Debugger -ScriptBlock $testScript + $results.Count | Should -Be 0 + } + + It '-ErrorAction Break does enter the debugger for in a nested function context under a DebuggerStepThrough block' { + $testScript = { + & { + [System.Diagnostics.DebuggerStepThrough()] + [CmdletBinding()] + param() + & { + [CmdletBinding()] + param() + 1/0 # We should be able to break here with -ErrorAction Break + } + } -ErrorAction Break + } + + $results = Test-Debugger -ScriptBlock $testScript + $results.Count | Should -Be 1 + $results[0] | ShouldHaveExtent -Line 9 -FromColumn 25 -ToColumn 28 + } + #> diff --git a/test/tools/Modules/HelpersDebugger/HelpersDebugger.psd1 b/test/tools/Modules/HelpersDebugger/HelpersDebugger.psd1 new file mode 100644 index 00000000000..19d88287c4f --- /dev/null +++ b/test/tools/Modules/HelpersDebugger/HelpersDebugger.psd1 @@ -0,0 +1,28 @@ +# 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' + + FunctionsToExport = @( + 'Get-DebuggerExtent' + 'Register-DebuggerHandler' + 'ShouldHaveExtent' + 'Test-Debugger' + 'Unregister-DebuggerHandler' + ) + + CmdletsToExport = @() + + AliasesToExport = @() +} diff --git a/test/tools/Modules/HelpersDebugger/HelpersDebugger.psm1 b/test/tools/Modules/HelpersDebugger/HelpersDebugger.psm1 new file mode 100644 index 00000000000..6fdd153a007 --- /dev/null +++ b/test/tools/Modules/HelpersDebugger/HelpersDebugger.psm1 @@ -0,0 +1,185 @@ +# 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()] + 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()] + 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()] + 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 = New-Object -TypeName System.InvalidOperationException -ArgumentList $message + $errorRecord = New-Object -TypeName System.Management.Automation.ErrorRecord -ArgumentList $exception,$exception.GetType().Name,'InvalidOperation' + 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 suppress any exceptions and silence any output because this + # invocation is about the debugger output, not the output of the script + # itself + try { [ScriptBlock]::Create($ScriptBlock).Invoke() *> $null } catch {} + $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 + } + } +} +Export-ModuleMember -Function Get-DebuggerExtent + +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 { + $callerEAP = $ErrorActionPreference + $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 From ab7d63676495a2974d87fb9e0cc82d07c4687052 Mon Sep 17 00:00:00 2001 From: Kirk Munro Date: Tue, 4 Jun 2019 20:18:30 -0300 Subject: [PATCH 2/7] additional Pester tests; fixed debugger bugs --- .../engine/debugger/debugger.cs | 6 - .../engine/lang/scriptblock.cs | 2 +- .../Debugging/DebuggerCommand.Tests.ps1 | 343 +++++++++++------- .../HelpersDebugger/HelpersDebugger.psd1 | 2 +- .../HelpersDebugger/HelpersDebugger.psm1 | 38 +- 5 files changed, 258 insertions(+), 133 deletions(-) diff --git a/src/System.Management.Automation/engine/debugger/debugger.cs b/src/System.Management.Automation/engine/debugger/debugger.cs index c5a7964ff70..d6a36cf94e2 100644 --- a/src/System.Management.Automation/engine/debugger/debugger.cs +++ b/src/System.Management.Automation/engine/debugger/debugger.cs @@ -1237,12 +1237,6 @@ internal void RemoveBreakpoint(Breakpoint breakpoint) breakpoint.RemoveSelf(this); - if (_idToBreakpoint.Count == 0) - { - // The last breakpoint was removed, turn off debugging. - SetInternalDebugMode(InternalDebugMode.Disabled); - } - OnBreakpointUpdated(new BreakpointUpdatedEventArgs(breakpoint, BreakpointUpdateType.Removed, _idToBreakpoint.Count)); } diff --git a/src/System.Management.Automation/engine/lang/scriptblock.cs b/src/System.Management.Automation/engine/lang/scriptblock.cs index 41adc90198d..1e455244105 100644 --- a/src/System.Management.Automation/engine/lang/scriptblock.cs +++ b/src/System.Management.Automation/engine/lang/scriptblock.cs @@ -109,7 +109,7 @@ public static ScriptBlock Create(string script) => Create( fileContents: script); internal static ScriptBlock CreateDelayParsedScriptBlock(string script, bool isProductCode) - => new ScriptBlock(new CompiledScriptBlockData(script, isProductCode)); + => new ScriptBlock(new CompiledScriptBlockData(script, isProductCode)) { DebuggerHidden = true }; /// /// Returns a new scriptblock bound to a module. Any local variables in the diff --git a/test/powershell/Language/Scripting/Debugging/DebuggerCommand.Tests.ps1 b/test/powershell/Language/Scripting/Debugging/DebuggerCommand.Tests.ps1 index 252323a42e2..900844f39cd 100644 --- a/test/powershell/Language/Scripting/Debugging/DebuggerCommand.Tests.ps1 +++ b/test/powershell/Language/Scripting/Debugging/DebuggerCommand.Tests.ps1 @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -Describe 'Simple debugger command tests' -tag 'CI' { +Describe 'Basic debugger command tests' -tag 'CI' { BeforeAll { Register-DebuggerHandler @@ -16,9 +16,7 @@ Describe 'Simple debugger command tests' -tag 'CI' { $testScript = { try { $bp = Set-PSBreakpoint -Command Get-Process - & { - Get-Process -Id $PID - } > $null + Get-Process -Id $PID > $null } finally { Remove-PSBreakPoint -Breakpoint $bp } @@ -60,9 +58,7 @@ Describe 'Simple debugger command tests' -tag 'CI' { $testScript = { try { $bp = Set-PSBreakpoint -Command Get-Process - & { - Get-Process -Id $PID - } > $null + Get-Process -Id $PID > $null } finally { Remove-PSBreakPoint -Breakpoint $bp } @@ -72,13 +68,11 @@ Describe 'Simple debugger command tests' -tag 'CI' { 1: 2: try { 3: $bp = Set-PSBreakpoint -Command Get-Process - 4: & { - 5:* Get-Process -Id $PID - 6: } > $null - 7: } finally { - 8: Remove-PSBreakPoint -Breakpoint $bp - 9: } - 10: + 4:* Get-Process -Id $PID > $null + 5: } finally { + 6: Remove-PSBreakPoint -Breakpoint $bp + 7: } + 8: '@ $results = @(Test-Debugger -ScriptBlock $testScript -CommandQueue 'l','list') @@ -105,14 +99,95 @@ Describe 'Simple debugger command tests' -tag 'CI' { } } + 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') + $resultl = if ($results.Count -gt 0) {$results[0].Output -replace '\s+$' -join [Environment]::NewLine -replace "^[`r`n]+|[`r`n]+$"} + $resultlist = 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 'Should only have non-empty string output from the list command' { + $results[0].Output | Should -BeOfType string + $resultl | Should -Match '\S' + } + + It '''l 4'' and ''list 4'' should show identical script listings' { + $resultl | Should -BeExactly $resultlist + } + + It 'Should show a partial script listing starting on line 4 with the current position on line 5' { + $resultl | 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') + $resultl = if ($results.Count -gt 0) {$results[0].Output -replace '\s+$' -join [Environment]::NewLine -replace "^[`r`n]+|[`r`n]+$"} + $resultlist = 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 'Should only have non-empty string output from the list command' { + $results[0].Output | Should -BeOfType string + $resultl | Should -Match '\S' + } + + It '''l 3 2'' and ''list 3 2'' should show identical script listings' { + $resultl | Should -BeExactly $resultlist + } + + It 'Should show a partial script listing showing 3 lines starting on line 4 with the current position on line 5' { + $resultl | 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 + Get-Process -Id $PID > $null } finally { Remove-PSBreakPoint -Breakpoint $bp } @@ -139,7 +214,7 @@ Describe 'Simple debugger command tests' -tag 'CI' { } -Describe 'Debugger stepping command tests' -tag 'CI' { +Describe 'Simple debugger stepping command tests' -tag 'CI' { BeforeAll { Register-DebuggerHandler @@ -154,12 +229,9 @@ Describe 'Debugger stepping command tests' -tag 'CI' { $testScript = { try { $bp = Set-PSBreakpoint -Command ForEach-Object - $sb = { + Get-Process -Id $PID | ForEach-Object { 'One fish, two fish' 'Red fish, blue fish' - } - & { - Get-Process -Id $PID | ForEach-Object -Process $sb } *> $null } finally { Remove-PSBreakPoint -Breakpoint $bp @@ -178,148 +250,175 @@ Describe 'Debugger stepping command tests' -tag 'CI' { It '''s'' and ''stepInto'' should have identical behaviour' { for ($i = 0; $i -lt 3; $i++) { - Get-DebuggerExtent -DebuggerCommandResult $results[$i] | Should -Be (Get-DebuggerExtent -DebuggerCommandResult $resultstepinto[$i]) + $results[$i] | ShouldHaveSameExtentAs -DebuggerCommandResult $resultstepinto[$i] } } It 'The first extent should be the statement containing ForEach-Object' { - $results[0] | ShouldHaveExtent -Line 9 -FromColumn 25 -ToColumn 75 + $results[0] | ShouldHaveExtent -FromLine 4 -FromColumn 21 -ToLine 7 -ToColumn 31 } It 'The second extent should be in the nested scriptblock' { - $results[1] | ShouldHaveExtent -Line 4 -FromColumn 27 -ToColumn 28 + $results[1] | ShouldHaveExtent -Line 4 -FromColumn 59 -ToColumn 60 } - It 'The third extent should be on Write-Object' { + It 'The third extent should be on ''One fish, two fish''' { $results[2] | ShouldHaveExtent -Line 5 -FromColumn 25 -ToColumn 45 } - It 'The fourth extent should be on ''Hello''' { + It 'The fourth extent should be on ''Red fish, blue fish''' { $results[3] | ShouldHaveExtent -Line 6 -FromColumn 25 -ToColumn 46 } } -} - <# - It '-ErrorAction Break enters the debugger on a terminating error' { + Context 'StepOver steps over the current command, unless it contains a triggerable breakpoint' { + BeforeAll { $testScript = { - & { - [CmdletBinding()] - param() - 'Hello' - Get-Process -Id ([int]::MaxValue + 1) - 'Goodbye' - } -ErrorAction Break + 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 + } } - $results = Test-Debugger -ScriptBlock $testScript - $results.Count | Should -Be 1 - $results[0] | ShouldHaveExtent -Line 6 -FromColumn 21 -ToColumn 58 + $resultv = @(Test-Debugger -ScriptBlock $testScript -CommandQueue 'v','v','v','v') + $resultstepover = @(Test-Debugger -ScriptBlock $testScript -CommandQueue 'stepOver','stepOver','stepOver','stepOver') } - It '-ErrorAction Break does NOT enter the debugger on a naked rethrow' { - $testScript = { - & { - [CmdletBinding()] - param() - try { - 'Hello' - Get-Process -Id ([int]::MaxValue) -ErrorAction Stop - 'Goodbye' - } catch { - throw - } - } -ErrorAction Break - } - - $results = Test-Debugger -ScriptBlock $testScript - $results.Count | Should -Be 1 - $results[0] | ShouldHaveExtent -Line 7 -FromColumn 25 -ToColumn 76 + It 'Should show 4 debugger commands were invoked twice' { + # One extra for the implicit 'c' command that keeps the debugger automation moving + $resultv.Count | Should -Be 5 + $resultstepover.Count | Should -Be 5 } - It '-ErrorAction Break does enter the debugger on a new throw' { - $testScript = { - & { - [CmdletBinding()] - param() - try { - 'Hello' - Get-Process -Id ([int]::MaxValue) -ErrorAction Stop - 'Goodbye' - } catch { - throw $_ - } - } -ErrorAction Break + It '''v'' and ''stepOver'' should have identical behaviour' { + for ($i = 0; $i -lt 3; $i++) { + $resultv[$i] | ShouldHaveSameExtentAs -DebuggerCommandResult $resultstepover[$i] } + } - $results = Test-Debugger -ScriptBlock $testScript - $results.Count | Should -Be 2 - $results[0] | ShouldHaveExtent -Line 7 -FromColumn 25 -ToColumn 76 - $results[1] | ShouldHaveExtent -Line 10 -FromColumn 25 -ToColumn 33 + It 'The first extent should be the statement containing ForEach-Object' { + $resultv[0] | ShouldHaveExtent -FromLine 5 -FromColumn 21 -ToLine 7 -ToColumn 31 } - It '-ErrorAction Break does NOT enter the debugger for errors inside a DebuggerHidden block' { - $testScript = { - & { - [System.Diagnostics.DebuggerHidden()] - [CmdletBinding()] - param() - 1/0 # We shouldn't be able to break here with -ErrorAction Break - } -ErrorAction Break - } + It 'The second extent should be on Enable-PSBreakpoint' { + $resultv[1] | ShouldHaveExtent -Line 8 -FromColumn 21 -ToColumn 57 + } - $results = Test-Debugger -ScriptBlock $testScript - $results.Count | Should -Be 0 + It 'The third extent should be on the script block invoked with the call operator' { + $resultv[2] | ShouldHaveExtent -FromLine 9 -FromColumn 21 -ToLine 11 -ToColumn 31 } - It '-ErrorAction Break does NOT enter the debugger for errors in a nested function context under a DebuggerHidden block' { + It 'The fourth extent should be on the ConvertTo-Csv breakpoint inside the script block' { + $resultv[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 = { - & { - [System.Diagnostics.DebuggerHidden()] - [CmdletBinding()] - param() + try { + $bps = Set-PSBreakpoint -Command Get-Process,ConvertTo-Csv & { - [CmdletBinding()] - param() - 1/0 # We shouldn't be able to break here with -ErrorAction Break + $process = Get-Process -Id $PID + $process.Id } - } -ErrorAction Break + $date = Get-Date + $date | ConvertTo-Csv + } finally { + Remove-PSBreakPoint -Breakpoint $bps + } } - $results = Test-Debugger -ScriptBlock $testScript - $results.Count | Should -Be 0 + $resulto = @(Test-Debugger -ScriptBlock $testScript -CommandQueue 'o','o','o') + $resultstepout = @(Test-Debugger -ScriptBlock $testScript -CommandQueue 'stepOut','stepOut','stepOut') } - It '-ErrorAction Break does NOT enter the debugger for errors inside a DebuggerStepThrough block' { - $testScript = { - & { - [System.Diagnostics.DebuggerStepThrough()] - [CmdletBinding()] - param() - 1/0 # We shouldn't be able to break here with -ErrorAction Break - } -ErrorAction Break + It 'Should show 3 debugger commands were invoked twice' { + # One extra for the implicit 'c' command that keeps the debugger automation moving + $resulto.Count | Should -Be 4 + $resultstepout.Count | Should -Be 4 + } + + It '''o'' and ''stepOut'' should have identical behaviour' { + for ($i = 0; $i -lt 3; $i++) { + $resulto[$i] | ShouldHaveSameExtentAs -DebuggerCommandResult $resultstepout[$i] } + } + + It 'The first extent should be on Get-Process' { + $resulto[0] | ShouldHaveExtent -Line 5 -FromColumn 25 -ToColumn 56 + } + + It 'The second extent should be on Get-Date' { + $resulto[1] | ShouldHaveExtent -Line 8 -FromColumn 21 -ToColumn 37 + } - $results = Test-Debugger -ScriptBlock $testScript - $results.Count | Should -Be 0 + It 'The third extent should be on the ConvertTo-Csv breakpoint' { + $resulto[2] | ShouldHaveExtent -Line 9 -FromColumn 21 -ToColumn 42 } + } +} + +Describe 'Debugger bug fix tests' -tag 'CI' { + + BeforeAll { + Register-DebuggerHandler + } + + AfterAll { + Unregister-DebuggerHandler + } - It '-ErrorAction Break does enter the debugger for in a nested function context under a DebuggerStepThrough block' { + Context 'Stepping works beyond Remove-PSBreakpoint (Issue #9824)' { + BeforeAll { $testScript = { - & { - [System.Diagnostics.DebuggerStepThrough()] - [CmdletBinding()] - param() - & { - [CmdletBinding()] - param() - 1/0 # We should be able to break here with -ErrorAction Break - } - } -ErrorAction Break + function Test-Issue9824 { + $bp = Set-PSBreakpoint -Command Remove-PSBreakpoint + Remove-PSBreakPoint -Breakpoint $bp + } + Test-Issue9824 + 1 + 1 } - $results = Test-Debugger -ScriptBlock $testScript - $results.Count | Should -Be 1 - $results[0] | ShouldHaveExtent -Line 9 -FromColumn 25 -ToColumn 28 + $results = @(Test-Debugger -ScriptBlock $testScript -CommandQueue 's','s','s') + $resultv = @(Test-Debugger -ScriptBlock $testScript -CommandQueue 'v','v','v') + $resulto = @(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 + $results.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 + $resultv.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 + $resulto.Count | Should -Be 3 + } + + It 'The last extent for stepInto should be on 1 + 1' { + $results[2] | ShouldHaveExtent -Line 7 -FromColumn 17 -ToColumn 22 + } + + It 'The last extent for stepOver should be on 1 + 1' { + $resultv[2] | ShouldHaveExtent -Line 7 -FromColumn 17 -ToColumn 22 } - #> + + It 'The last extent for stepOut should be on 1 + 1' { + $resulto[1] | ShouldHaveExtent -Line 7 -FromColumn 17 -ToColumn 22 + } + } +} diff --git a/test/tools/Modules/HelpersDebugger/HelpersDebugger.psd1 b/test/tools/Modules/HelpersDebugger/HelpersDebugger.psd1 index 19d88287c4f..ea6e4f8d68a 100644 --- a/test/tools/Modules/HelpersDebugger/HelpersDebugger.psd1 +++ b/test/tools/Modules/HelpersDebugger/HelpersDebugger.psd1 @@ -15,9 +15,9 @@ Description = 'Helper module for Pester tests that automate the debugger' FunctionsToExport = @( - 'Get-DebuggerExtent' 'Register-DebuggerHandler' 'ShouldHaveExtent' + 'ShouldHaveSameExtentAs' 'Test-Debugger' 'Unregister-DebuggerHandler' ) diff --git a/test/tools/Modules/HelpersDebugger/HelpersDebugger.psm1 b/test/tools/Modules/HelpersDebugger/HelpersDebugger.psm1 index 6fdd153a007..30ca8c02b62 100644 --- a/test/tools/Modules/HelpersDebugger/HelpersDebugger.psm1 +++ b/test/tools/Modules/HelpersDebugger/HelpersDebugger.psm1 @@ -110,7 +110,17 @@ function Test-Debugger { # We also suppress any exceptions and silence any output because this # invocation is about the debugger output, not the output of the script # itself - try { [ScriptBlock]::Create($ScriptBlock).Invoke() *> $null } catch {} + & { + [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 @@ -138,7 +148,6 @@ function Get-DebuggerExtent { } } } -Export-ModuleMember -Function Get-DebuggerExtent function ShouldHaveExtent { [CmdletBinding(DefaultParameterSetName='SingleLineExtent')] @@ -174,7 +183,6 @@ function ShouldHaveExtent { $ToColumn ) process { - $callerEAP = $ErrorActionPreference $extent = Get-DebuggerExtent -DebuggerCommandResult $DebuggerCommandResult $extent.StartLineNumber | Should -Be $(if ($PSCmdlet.ParameterSetName -eq 'SingleLineExtent') {$Line} else {$FromLine}) $extent.StartColumnNumber | Should -Be $FromColumn @@ -183,3 +191,27 @@ function ShouldHaveExtent { } } 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 From 1a84038043726b0cb7caf88f52eaf921b282e060 Mon Sep 17 00:00:00 2001 From: Kirk Munro Date: Wed, 5 Jun 2019 10:40:28 -0300 Subject: [PATCH 3/7] make variable names in tests easier to understand --- .../Debugging/DebuggerCommand.Tests.ps1 | 180 ++++++++++-------- 1 file changed, 99 insertions(+), 81 deletions(-) diff --git a/test/powershell/Language/Scripting/Debugging/DebuggerCommand.Tests.ps1 b/test/powershell/Language/Scripting/Debugging/DebuggerCommand.Tests.ps1 index 900844f39cd..399c4927e82 100644 --- a/test/powershell/Language/Scripting/Debugging/DebuggerCommand.Tests.ps1 +++ b/test/powershell/Language/Scripting/Debugging/DebuggerCommand.Tests.ps1 @@ -23,8 +23,10 @@ Describe 'Basic debugger command tests' -tag 'CI' { } $results = @(Test-Debugger -ScriptBlock $testScript -CommandQueue '?','h') - $result? = if ($results.Count -gt 0) {$results[0].Output -join [Environment]::NewLine} - $resulth = if ($results.Count -gt 1) {$results[1].Output -join [Environment]::NewLine} + $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' { @@ -32,25 +34,25 @@ Describe 'Basic debugger command tests' -tag 'CI' { $results.Count | Should -Be 3 } - It 'Should only have non-empty string output from the help command' { - $results[0].Output | Should -BeOfType string - $result? | Should -Match '\S' + It '''h'' and ''?'' should show identical help messages' { + $result['?'] | Should -BeExactly $result['h'] } - It '''h'' and ''?'' should show identical help messages' { - $result? | Should -BeExactly $resulth - } - - 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 ' {$result? | Should -Match '\s\s+'} - It 'Should show help for help' {$result? | Should -Match '\s\?, h\s+'} + 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 ' {$result['?'] | Should -Match '\s\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' { @@ -76,8 +78,10 @@ Describe 'Basic debugger command tests' -tag 'CI' { '@ $results = @(Test-Debugger -ScriptBlock $testScript -CommandQueue 'l','list') - $resultl = if ($results.Count -gt 0) {$results[0].Output -replace '\s+$' -join [Environment]::NewLine -replace "^[`r`n]+|[`r`n]+$"} - $resultlist = if ($results.Count -gt 1) {$results[1].Output -replace '\s+$' -join [Environment]::NewLine -replace "^[`r`n]+|[`r`n]+$"} + $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' { @@ -85,17 +89,17 @@ Describe 'Basic debugger command tests' -tag 'CI' { $results.Count | Should -Be 3 } - It 'Should only have non-empty string output from the list command' { - $results[0].Output | Should -BeOfType string - $resultl | Should -Match '\S' + It '''l'' and ''list'' should show identical script listings' { + $result['l'] | Should -BeExactly $result['list'] } - It '''l'' and ''list'' should show identical script listings' { - $resultl | Should -BeExactly $resultlist + 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' { - $resultl | Should -BeExactly $testScriptList + $result['l'] | Should -BeExactly $testScriptList } } @@ -119,8 +123,10 @@ Describe 'Basic debugger command tests' -tag 'CI' { '@ $results = @(Test-Debugger -ScriptBlock $testScript -CommandQueue 'l 4','list 4') - $resultl = if ($results.Count -gt 0) {$results[0].Output -replace '\s+$' -join [Environment]::NewLine -replace "^[`r`n]+|[`r`n]+$"} - $resultlist = if ($results.Count -gt 1) {$results[1].Output -replace '\s+$' -join [Environment]::NewLine -replace "^[`r`n]+|[`r`n]+$"} + $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' { @@ -128,17 +134,17 @@ Describe 'Basic debugger command tests' -tag 'CI' { $results.Count | Should -Be 3 } - It 'Should only have non-empty string output from the list command' { - $results[0].Output | Should -BeOfType string - $resultl | Should -Match '\S' + It '''l 4'' and ''list 4'' should show identical script listings' { + $result['l 4'] | Should -BeExactly $result['list 4'] } - It '''l 4'' and ''list 4'' should show identical script listings' { - $resultl | Should -BeExactly $resultlist + 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' { - $resultl | Should -BeExactly $testScriptList + $result['l 4'] | Should -BeExactly $testScriptList } } @@ -159,8 +165,10 @@ Describe 'Basic debugger command tests' -tag 'CI' { '@ $results = @(Test-Debugger -ScriptBlock $testScript -CommandQueue 'l 3 2','list 3 2') - $resultl = if ($results.Count -gt 0) {$results[0].Output -replace '\s+$' -join [Environment]::NewLine -replace "^[`r`n]+|[`r`n]+$"} - $resultlist = if ($results.Count -gt 1) {$results[1].Output -replace '\s+$' -join [Environment]::NewLine -replace "^[`r`n]+|[`r`n]+$"} + $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' { @@ -168,17 +176,17 @@ Describe 'Basic debugger command tests' -tag 'CI' { $results.Count | Should -Be 3 } - It 'Should only have non-empty string output from the list command' { - $results[0].Output | Should -BeOfType string - $resultl | Should -Match '\S' + It '''l 3 2'' and ''list 3 2'' should show identical script listings' { + $result['l 3 2'] | Should -BeExactly $result['list 3 2'] } - It '''l 3 2'' and ''list 3 2'' should show identical script listings' { - $resultl | Should -BeExactly $resultlist + 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' { - $resultl | Should -BeExactly $testScriptList + $result['l 3 2'] | Should -BeExactly $testScriptList } } @@ -194,8 +202,10 @@ Describe 'Basic debugger command tests' -tag 'CI' { } $results = @(Test-Debugger -ScriptBlock $testScript -CommandQueue 'k','Get-PSCallStack') - $resultk = if ($results.Count -gt 0) {$results[0].Output} - $resultgcs = if ($results.Count -gt 1) {$results[1].Output} + $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' { @@ -208,7 +218,7 @@ Describe 'Basic debugger command tests' -tag 'CI' { } It '''k'' and ''Get-PSCallStack'' should show identical script listings' { - [string[]]$resultk -join [Environment]::NewLine | Should -BeExactly ([string[]]$resultgcs -join [Environment]::NewLine) + [string[]]$result['k'] -join [Environment]::NewLine | Should -BeExactly ([string[]]$result['Get-PSCallStack'] -join [Environment]::NewLine) } } @@ -238,36 +248,38 @@ Describe 'Simple debugger stepping command tests' -tag 'CI' { } } - $results = @(Test-Debugger -ScriptBlock $testScript -CommandQueue 's','s','s','s') - $resultstepinto = @(Test-Debugger -ScriptBlock $testScript -CommandQueue 'stepInto','stepInto','stepInto','stepInto') + $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 - $results.Count | Should -Be 5 - $resultstepinto.Count | Should -Be 5 + $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++) { - $results[$i] | ShouldHaveSameExtentAs -DebuggerCommandResult $resultstepinto[$i] + $result['s'][$i] | ShouldHaveSameExtentAs -DebuggerCommandResult $result['stepInto'][$i] } } It 'The first extent should be the statement containing ForEach-Object' { - $results[0] | ShouldHaveExtent -FromLine 4 -FromColumn 21 -ToLine 7 -ToColumn 31 + $result['s'][0] | ShouldHaveExtent -FromLine 4 -FromColumn 21 -ToLine 7 -ToColumn 31 } It 'The second extent should be in the nested scriptblock' { - $results[1] | ShouldHaveExtent -Line 4 -FromColumn 59 -ToColumn 60 + $result['s'][1] | ShouldHaveExtent -Line 4 -FromColumn 59 -ToColumn 60 } It 'The third extent should be on ''One fish, two fish''' { - $results[2] | ShouldHaveExtent -Line 5 -FromColumn 25 -ToColumn 45 + $result['s'][2] | ShouldHaveExtent -Line 5 -FromColumn 25 -ToColumn 45 } It 'The fourth extent should be on ''Red fish, blue fish''' { - $results[3] | ShouldHaveExtent -Line 6 -FromColumn 25 -ToColumn 46 + $result['s'][3] | ShouldHaveExtent -Line 6 -FromColumn 25 -ToColumn 46 } } @@ -289,36 +301,38 @@ Describe 'Simple debugger stepping command tests' -tag 'CI' { } } - $resultv = @(Test-Debugger -ScriptBlock $testScript -CommandQueue 'v','v','v','v') - $resultstepover = @(Test-Debugger -ScriptBlock $testScript -CommandQueue 'stepOver','stepOver','stepOver','stepOver') + $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 - $resultv.Count | Should -Be 5 - $resultstepover.Count | Should -Be 5 + $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++) { - $resultv[$i] | ShouldHaveSameExtentAs -DebuggerCommandResult $resultstepover[$i] + $result['v'][$i] | ShouldHaveSameExtentAs -DebuggerCommandResult $result['stepOver'][$i] } } It 'The first extent should be the statement containing ForEach-Object' { - $resultv[0] | ShouldHaveExtent -FromLine 5 -FromColumn 21 -ToLine 7 -ToColumn 31 + $result['v'][0] | ShouldHaveExtent -FromLine 5 -FromColumn 21 -ToLine 7 -ToColumn 31 } It 'The second extent should be on Enable-PSBreakpoint' { - $resultv[1] | ShouldHaveExtent -Line 8 -FromColumn 21 -ToColumn 57 + $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' { - $resultv[2] | ShouldHaveExtent -FromLine 9 -FromColumn 21 -ToLine 11 -ToColumn 31 + $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' { - $resultv[3] | ShouldHaveExtent -Line 10 -FromColumn 25 -ToColumn 49 + $result['v'][3] | ShouldHaveExtent -Line 10 -FromColumn 25 -ToColumn 49 } } @@ -338,32 +352,34 @@ Describe 'Simple debugger stepping command tests' -tag 'CI' { } } - $resulto = @(Test-Debugger -ScriptBlock $testScript -CommandQueue 'o','o','o') - $resultstepout = @(Test-Debugger -ScriptBlock $testScript -CommandQueue 'stepOut','stepOut','stepOut') + $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 - $resulto.Count | Should -Be 4 - $resultstepout.Count | Should -Be 4 + $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++) { - $resulto[$i] | ShouldHaveSameExtentAs -DebuggerCommandResult $resultstepout[$i] + $result['o'][$i] | ShouldHaveSameExtentAs -DebuggerCommandResult $result['stepOut'][$i] } } It 'The first extent should be on Get-Process' { - $resulto[0] | ShouldHaveExtent -Line 5 -FromColumn 25 -ToColumn 56 + $result['o'][0] | ShouldHaveExtent -Line 5 -FromColumn 25 -ToColumn 56 } It 'The second extent should be on Get-Date' { - $resulto[1] | ShouldHaveExtent -Line 8 -FromColumn 21 -ToColumn 37 + $result['o'][1] | ShouldHaveExtent -Line 8 -FromColumn 21 -ToColumn 37 } It 'The third extent should be on the ConvertTo-Csv breakpoint' { - $resulto[2] | ShouldHaveExtent -Line 9 -FromColumn 21 -ToColumn 42 + $result['o'][2] | ShouldHaveExtent -Line 9 -FromColumn 21 -ToColumn 42 } } } @@ -389,36 +405,38 @@ Describe 'Debugger bug fix tests' -tag 'CI' { 1 + 1 } - $results = @(Test-Debugger -ScriptBlock $testScript -CommandQueue 's','s','s') - $resultv = @(Test-Debugger -ScriptBlock $testScript -CommandQueue 'v','v','v') - $resulto = @(Test-Debugger -ScriptBlock $testScript -CommandQueue 'o','o') + $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 - $results.Count | Should -Be 4 + $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 - $resultv.Count | Should -Be 4 + $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 - $resulto.Count | Should -Be 3 + $result['o'].Count | Should -Be 3 } It 'The last extent for stepInto should be on 1 + 1' { - $results[2] | ShouldHaveExtent -Line 7 -FromColumn 17 -ToColumn 22 + $result['s'][2] | ShouldHaveExtent -Line 7 -FromColumn 17 -ToColumn 22 } It 'The last extent for stepOver should be on 1 + 1' { - $resultv[2] | ShouldHaveExtent -Line 7 -FromColumn 17 -ToColumn 22 + $result['v'][2] | ShouldHaveExtent -Line 7 -FromColumn 17 -ToColumn 22 } It 'The last extent for stepOut should be on 1 + 1' { - $resulto[1] | ShouldHaveExtent -Line 7 -FromColumn 17 -ToColumn 22 + $result['o'][1] | ShouldHaveExtent -Line 7 -FromColumn 17 -ToColumn 22 } } } From 7d7766dc21cb3cf0042075acae432bbbae91ed83 Mon Sep 17 00:00:00 2001 From: Kirk Munro Date: Wed, 5 Jun 2019 12:49:20 -0300 Subject: [PATCH 4/7] put disable debugger back with new conditions --- .../engine/debugger/debugger.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/System.Management.Automation/engine/debugger/debugger.cs b/src/System.Management.Automation/engine/debugger/debugger.cs index d6a36cf94e2..e19b3f2ce25 100644 --- a/src/System.Management.Automation/engine/debugger/debugger.cs +++ b/src/System.Management.Automation/engine/debugger/debugger.cs @@ -1237,6 +1237,18 @@ internal void RemoveBreakpoint(Breakpoint breakpoint) breakpoint.RemoveSelf(this); + if (_idToBreakpoint.Count == 0 && + _currentDebuggerAction != DebuggerResumeAction.StepInto && + _currentDebuggerAction != DebuggerResumeAction.StepOver && + _currentDebuggerAction != DebuggerResumeAction.StepOut) + { + // Turn off debugging if the last breakpoint was removed, + // and if we are not currently stepping in the debugger. + // This allows the remainder of the script to run more + // efficiently. + SetInternalDebugMode(InternalDebugMode.Disabled); + } + OnBreakpointUpdated(new BreakpointUpdatedEventArgs(breakpoint, BreakpointUpdateType.Removed, _idToBreakpoint.Count)); } From 0580771af8a7695ae21ca7883ce7a4f2e5b22106 Mon Sep 17 00:00:00 2001 From: Kirk Munro Date: Wed, 5 Jun 2019 13:03:46 -0300 Subject: [PATCH 5/7] Codacy update --- test/tools/Modules/HelpersDebugger/HelpersDebugger.psm1 | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/tools/Modules/HelpersDebugger/HelpersDebugger.psm1 b/test/tools/Modules/HelpersDebugger/HelpersDebugger.psm1 index 30ca8c02b62..0f7ba9ed1fd 100644 --- a/test/tools/Modules/HelpersDebugger/HelpersDebugger.psm1 +++ b/test/tools/Modules/HelpersDebugger/HelpersDebugger.psm1 @@ -48,6 +48,7 @@ $debuggerStopHandlerRegistered = $false function Register-DebuggerHandler { [CmdletBinding()] + [OutputType([System.Void])] param() try { $callerEAP = $ErrorActionPreference @@ -65,6 +66,7 @@ Export-ModuleMember -Function Register-DebuggerHandler function Unregister-DebuggerHandler { [CmdletBinding()] + [OutputType([System.Void])] param() try { $callerEAP = $ErrorActionPreference @@ -79,6 +81,7 @@ Export-ModuleMember -Function Unregister-DebuggerHandler function Test-Debugger { [CmdletBinding()] + [OutputType('DebuggerCommandResult')] param( [Parameter(Position=0, Mandatory)] [ValidateNotNullOrEmpty()] From 4aafbb39cd02fb7ec421b3775e4b7eaf11970795 Mon Sep 17 00:00:00 2001 From: Kirk Munro Date: Wed, 5 Jun 2019 16:13:00 -0300 Subject: [PATCH 6/7] refactor disable debugger test into property --- .../engine/debugger/debugger.cs | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/src/System.Management.Automation/engine/debugger/debugger.cs b/src/System.Management.Automation/engine/debugger/debugger.cs index e19b3f2ce25..d89b5f7c1bb 100644 --- a/src/System.Management.Automation/engine/debugger/debugger.cs +++ b/src/System.Management.Automation/engine/debugger/debugger.cs @@ -1237,15 +1237,8 @@ internal void RemoveBreakpoint(Breakpoint breakpoint) breakpoint.RemoveSelf(this); - if (_idToBreakpoint.Count == 0 && - _currentDebuggerAction != DebuggerResumeAction.StepInto && - _currentDebuggerAction != DebuggerResumeAction.StepOver && - _currentDebuggerAction != DebuggerResumeAction.StepOut) - { - // Turn off debugging if the last breakpoint was removed, - // and if we are not currently stepping in the debugger. - // This allows the remainder of the script to run more - // efficiently. + if (CanDisableDebugger) + { SetInternalDebugMode(InternalDebugMode.Disabled); } @@ -2105,6 +2098,19 @@ private bool CanEnableDebugger } } + 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 @@ -3863,9 +3869,8 @@ internal void DisableTracing() _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); } } From 14d5f1128059fa8afdeddd79e636c58c6d9c025d Mon Sep 17 00:00:00 2001 From: Kirk Munro Date: Wed, 12 Jun 2019 15:58:33 -0300 Subject: [PATCH 7/7] update based on PR feedback --- test/tools/Modules/HelpersDebugger/HelpersDebugger.psd1 | 2 ++ test/tools/Modules/HelpersDebugger/HelpersDebugger.psm1 | 9 ++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/test/tools/Modules/HelpersDebugger/HelpersDebugger.psd1 b/test/tools/Modules/HelpersDebugger/HelpersDebugger.psd1 index ea6e4f8d68a..cc5d11ab936 100644 --- a/test/tools/Modules/HelpersDebugger/HelpersDebugger.psd1 +++ b/test/tools/Modules/HelpersDebugger/HelpersDebugger.psd1 @@ -14,6 +14,8 @@ Description = 'Helper module for Pester tests that automate the debugger' + PowerShellVersion = '5.0' + FunctionsToExport = @( 'Register-DebuggerHandler' 'ShouldHaveExtent' diff --git a/test/tools/Modules/HelpersDebugger/HelpersDebugger.psm1 b/test/tools/Modules/HelpersDebugger/HelpersDebugger.psm1 index 0f7ba9ed1fd..79cafc91a92 100644 --- a/test/tools/Modules/HelpersDebugger/HelpersDebugger.psm1 +++ b/test/tools/Modules/HelpersDebugger/HelpersDebugger.psm1 @@ -99,8 +99,8 @@ function Test-Debugger { # 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 = New-Object -TypeName System.InvalidOperationException -ArgumentList $message - $errorRecord = New-Object -TypeName System.Management.Automation.ErrorRecord -ArgumentList $exception,$exception.GetType().Name,'InvalidOperation' + $exception = [System.InvalidOperationException]::new($message) + $errorRecord = [System.Management.Automation.ErrorRecord]::new($exception, $exception.GetType().Name, 'InvalidOperation', $null) throw $errorRecord } $script:dbgResults = @() @@ -110,9 +110,8 @@ function Test-Debugger { } # 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 suppress any exceptions and silence any output because this - # invocation is about the debugger output, not the output of the script - # itself + # 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()]