From c64ca5c001d95132ae45d635bfd06aaeb3173262 Mon Sep 17 00:00:00 2001 From: Andrew Schwartzmeyer Date: Thu, 12 Aug 2021 15:15:44 -0700 Subject: [PATCH] Add regression test for `System.Windows.Forms` bug --- .../Debugging/FormsTest.ps1 | 5 ++ .../Debugging/DebugServiceTests.cs | 90 +++++++++++++------ 2 files changed, 70 insertions(+), 25 deletions(-) create mode 100644 test/PowerShellEditorServices.Test.Shared/Debugging/FormsTest.ps1 diff --git a/test/PowerShellEditorServices.Test.Shared/Debugging/FormsTest.ps1 b/test/PowerShellEditorServices.Test.Shared/Debugging/FormsTest.ps1 new file mode 100644 index 0000000000..68371922c5 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Debugging/FormsTest.ps1 @@ -0,0 +1,5 @@ +Add-Type -AssemblyName "System.Windows.Forms" +$before = "before" +$form = New-Object System.Windows.Forms.Form +$after = "after" +Write-Host "$before, $form, $after" diff --git a/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs b/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs index b3cedbd141..e112fc3067 100644 --- a/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs +++ b/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs @@ -32,6 +32,16 @@ public class DebugServiceTests : IDisposable private AsyncQueue sessionStateQueue = new AsyncQueue(); + private ScriptFile GetDebugScript(string fileName) + { + return this.workspace.GetFile( + TestUtilities.NormalizePath(Path.Combine( + Path.GetDirectoryName(typeof(DebugServiceTests).Assembly.Location), + "../../../../PowerShellEditorServices.Test.Shared/Debugging", + fileName + ))); + } + public DebugServiceTests() { var logger = NullLogger.Instance; @@ -41,18 +51,9 @@ public DebugServiceTests() this.workspace = new WorkspaceService(NullLoggerFactory.Instance); - // Load the test debug file - this.debugScriptFile = - this.workspace.GetFile( - TestUtilities.NormalizePath(Path.Combine( - Path.GetDirectoryName(typeof(DebugServiceTests).Assembly.Location), - "../../../../PowerShellEditorServices.Test.Shared/Debugging/VariableTest.ps1"))); - - this.variableScriptFile = - this.workspace.GetFile( - TestUtilities.NormalizePath(Path.Combine( - Path.GetDirectoryName(typeof(DebugServiceTests).Assembly.Location), - "../../../../PowerShellEditorServices.Test.Shared/Debugging/VariableTest.ps1"))); + // Load the test debug files + this.debugScriptFile = GetDebugScript("DebugTest.ps1"); + this.variableScriptFile = GetDebugScript("VariableTest.ps1"); this.debugService = new DebugService( this.powerShellContext, @@ -65,13 +66,6 @@ public DebugServiceTests() this.debugService.DebuggerStopped += debugService_DebuggerStopped; this.debugService.BreakpointUpdated += debugService_BreakpointUpdated; - - // Load the test debug file - this.debugScriptFile = - this.workspace.GetFile( - TestUtilities.NormalizePath(Path.Combine( - Path.GetDirectoryName(typeof(DebugServiceTests).Assembly.Location), - "../../../../PowerShellEditorServices.Test.Shared/Debugging/DebugTest.ps1"))); } async void powerShellContext_SessionStateChanged(object sender, SessionStateChangedEventArgs e) @@ -123,11 +117,7 @@ public async Task DebuggerAcceptsScriptArgs(string[] args) // The path is intentionally odd (some escaped chars but not all) because we are testing // the internal path escaping mechanism - it should escape certains chars ([, ] and space) but // it should not escape already escaped chars. - ScriptFile debugWithParamsFile = - this.workspace.GetFile( - TestUtilities.NormalizePath(Path.Combine( - Path.GetDirectoryName(typeof(DebugServiceTests).Assembly.Location), - "../../../../PowerShellEditorServices.Test.Shared/Debugging/Debug W&ith Params [Test].ps1"))); + ScriptFile debugWithParamsFile = GetDebugScript("Debug W&ith Params [Test].ps1"); await this.debugService.SetLineBreakpointsAsync( debugWithParamsFile, @@ -889,7 +879,7 @@ await this.debugService.SetLineBreakpointsAsync( var nullStringVar = variables.FirstOrDefault(v => v.Name == "$nullString"); Assert.NotNull(nullStringVar); - Assert.True("[NullString]".Equals(nullStringVar.ValueString)); + Assert.Equal("[NullString]", nullStringVar.ValueString); Assert.True(nullStringVar.IsExpandable); // Abort script execution early and wait for completion @@ -973,6 +963,7 @@ await this.debugService.SetLineBreakpointsAsync( // Verifies fix for issue #86, $proc = Get-Process foo displays just the ETS property set // and not all process properties. + [Trait("Category", "DebugService")] [Fact] public async Task DebuggerVariableProcessObjDisplaysCorrectly() { @@ -1005,6 +996,55 @@ await this.debugService.SetLineBreakpointsAsync( await executeTask.ConfigureAwait(false); } + // This is a regression test for a bug where user code causes a new synchronization context + // to be created, breaking the extension. It's most evident when debugging PowerShell + // scripts that use System.Windows.Forms. It required fixing both Editor Services and + // OmniSharp. + // + // This test depends on PowerShell being able to load System.Windows.Forms, which only works + // reliably with Windows PowerShell. It works with PowerShell Core in the real-world; + // however, our host executable is xUnit, not PowerShell. So by restricting to Windows + // PowerShell, we avoid all issues with our test project (and the xUnit executable) not + // having System.Windows.Forms deployed, and can instead rely on the Windows Global Assembly + // Cache (GAC) to find it. + [Trait("Category", "DebugService")] + [SkippableFact] + public async Task DebuggerStepsPastSystemWindowsForms() + { + Skip.If(!VersionUtils.IsPS5); + + ScriptFile formsFile = GetDebugScript("FormsTest.ps1"); + + await debugService.SetCommandBreakpointsAsync( + new [] { CommandBreakpointDetails.Create("Write-Host") }).ConfigureAwait(false); + + Task executeTask = powerShellContext.ExecuteScriptWithArgsAsync(formsFile.FilePath); + + // Wait for function breakpoint to hit + await AssertDebuggerStopped(formsFile.FilePath, 5).ConfigureAwait(false); + + StackFrameDetails[] stackFrames = debugService.GetStackFrames(); + VariableDetailsBase[] variables = + debugService.GetVariables(stackFrames[0].LocalVariables.Id); + + // Verify $before, $form, and $after vars exist + var before = variables.FirstOrDefault(v => v.Name == "$before"); + Assert.NotNull(before); + Assert.Equal("\"before\"", before.ValueString); + + var form = variables.FirstOrDefault(v => v.Name == "$form"); + Assert.NotNull(form); + Assert.StartsWith("System.Windows.Forms.Form", form.ValueString); + + var after = variables.FirstOrDefault(v => v.Name == "$after"); + Assert.NotNull(after); + Assert.Equal("\"after\"", after.ValueString); + + // Abort script execution early and wait for completion + debugService.Abort(); + await executeTask.ConfigureAwait(false); + } + public async Task AssertDebuggerPaused() { DebuggerStoppedEventArgs eventArgs =