From aac4c6ff2184d3fb096290372fd51d70ce98ea2b Mon Sep 17 00:00:00 2001 From: Kirk Munro Date: Thu, 13 Jun 2019 11:19:13 -0300 Subject: [PATCH] Add module to support Pester tests for automating debugger commands (stepInto, stepOut, etc.), along with basic tests (#9825) --- .../engine/debugger/debugger.cs | 19 +- .../engine/lang/scriptblock.cs | 2 +- .../Debugging/DebuggerCommand.Tests.ps1 | 442 ++++++++++++++++++ .../HelpersDebugger/HelpersDebugger.psd1 | 30 ++ .../HelpersDebugger/HelpersDebugger.psm1 | 219 +++++++++ 5 files changed, 707 insertions(+), 5 deletions(-) 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/src/System.Management.Automation/engine/debugger/debugger.cs b/src/System.Management.Automation/engine/debugger/debugger.cs index c5a7964ff..d89b5f7c1 100644 --- a/src/System.Management.Automation/engine/debugger/debugger.cs +++ b/src/System.Management.Automation/engine/debugger/debugger.cs @@ -1237,9 +1237,8 @@ namespace System.Management.Automation breakpoint.RemoveSelf(this); - if (_idToBreakpoint.Count == 0) + if (CanDisableDebugger) { - // The last breakpoint was removed, turn off debugging. SetInternalDebugMode(InternalDebugMode.Disabled); } @@ -2099,6 +2098,19 @@ namespace System.Management.Automation } } + private bool CanDisableDebugger + { + get + { + // The debugger can be disbled if there are no breakpoints + // left and if we are not currently stepping in the debugger. + return _idToBreakpoint.Count == 0 && + _currentDebuggerAction != DebuggerResumeAction.StepInto && + _currentDebuggerAction != DebuggerResumeAction.StepOver && + _currentDebuggerAction != DebuggerResumeAction.StepOut; + } + } + private static bool IsSystemLockedDown { get @@ -3857,9 +3869,8 @@ namespace System.Management.Automation _context.IgnoreScriptDebug = _savedIgnoreScriptDebug; _context.PSDebugTraceLevel = 0; _context.PSDebugTraceStep = false; - if (!_idToBreakpoint.Any()) + if (CanDisableDebugger) { - // Only disable debug mode if there are no breakpoints. SetInternalDebugMode(InternalDebugMode.Disabled); } } diff --git a/src/System.Management.Automation/engine/lang/scriptblock.cs b/src/System.Management.Automation/engine/lang/scriptblock.cs index 41adc9019..1e4552441 100644 --- a/src/System.Management.Automation/engine/lang/scriptblock.cs +++ b/src/System.Management.Automation/engine/lang/scriptblock.cs @@ -109,7 +109,7 @@ namespace System.Management.Automation fileContents: script); internal static ScriptBlock CreateDelayParsedScriptBlock(string script, bool isProductCode) - => new ScriptBlock(new CompiledScriptBlockData(script, isProductCode)); + => new ScriptBlock(new CompiledScriptBlockData(script, isProductCode)) { DebuggerHidden = true }; /// /// 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 new file mode 100644 index 000000000..399c4927e --- /dev/null +++ b/test/powershell/Language/Scripting/Debugging/DebuggerCommand.Tests.ps1 @@ -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 ' {$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:* 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 + } + } +} diff --git a/test/tools/Modules/HelpersDebugger/HelpersDebugger.psd1 b/test/tools/Modules/HelpersDebugger/HelpersDebugger.psd1 new file mode 100644 index 000000000..cc5d11ab9 --- /dev/null +++ b/test/tools/Modules/HelpersDebugger/HelpersDebugger.psd1 @@ -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 = @() +} diff --git a/test/tools/Modules/HelpersDebugger/HelpersDebugger.psm1 b/test/tools/Modules/HelpersDebugger/HelpersDebugger.psm1 new file mode 100644 index 000000000..79cafc91a --- /dev/null +++ b/test/tools/Modules/HelpersDebugger/HelpersDebugger.psm1 @@ -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