diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs index 2cfc24a2d..14bd5389e 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs @@ -102,14 +102,17 @@ public Task Handle(ConfigurationDoneArguments request return Task.FromResult(new ConfigurationDoneResponse()); } - private async Task LaunchScriptAsync(string scriptToLaunch) + // NOTE: We test this function in `DebugServiceTests` so it both needs to be internal, and + // use conditional-access on `_debugStateService` and `_debugAdapterServer` as its not set + // by tests. + internal async Task LaunchScriptAsync(string scriptToLaunch) { PSCommand command; if (System.IO.File.Exists(scriptToLaunch)) { // For a saved file we just execute its path (after escaping it). command = PSCommandHelpers.BuildDotSourceCommandWithArguments( - string.Concat('"', scriptToLaunch, '"'), _debugStateService.Arguments); + string.Concat('"', scriptToLaunch, '"'), _debugStateService?.Arguments); } else // It's a URI to an untitled script, or a raw script. { @@ -135,7 +138,7 @@ private async Task LaunchScriptAsync(string scriptToLaunch) // on each invocation, so passing the user's arguments directly in the initial // `AddScript` surprisingly works. command = PSCommandHelpers - .BuildDotSourceCommandWithArguments("$args[0]", _debugStateService.Arguments) + .BuildDotSourceCommandWithArguments("$args[0]", _debugStateService?.Arguments) .AddArgument(ast.GetScriptBlock()); } else @@ -148,7 +151,7 @@ private async Task LaunchScriptAsync(string scriptToLaunch) "{" + System.Environment.NewLine, isScriptFile ? untitledScript.Contents : scriptToLaunch, System.Environment.NewLine + "}"), - _debugStateService.Arguments); + _debugStateService?.Arguments); } } @@ -156,7 +159,8 @@ await _executionService.ExecutePSCommandAsync( command, CancellationToken.None, s_debuggerExecutionOptions).ConfigureAwait(false); - _debugAdapterServer.SendNotification(EventNames.Terminated); + + _debugAdapterServer?.SendNotification(EventNames.Terminated); } } } diff --git a/test/PowerShellEditorServices.Test.E2E/DebugAdapterProtocolMessageTests.cs b/test/PowerShellEditorServices.Test.E2E/DebugAdapterProtocolMessageTests.cs index 7505ad89e..553024856 100644 --- a/test/PowerShellEditorServices.Test.E2E/DebugAdapterProtocolMessageTests.cs +++ b/test/PowerShellEditorServices.Test.E2E/DebugAdapterProtocolMessageTests.cs @@ -305,9 +305,8 @@ public async Task CanStepPastSystemWindowsForms() [Fact] public async Task CanLaunchScriptWithCommentedLastLineAsync() { - string script = GenerateScriptFromLoggingStatements("a log statement") + "# a comment at the end"; - Assert.Contains(Environment.NewLine + "# a comment", script); - Assert.EndsWith("at the end", script); + string script = GenerateScriptFromLoggingStatements("$($MyInvocation.Line)") + "# a comment at the end"; + Assert.EndsWith(Environment.NewLine + "# a comment at the end", script); // NOTE: This is horribly complicated, but the "script" parameter here is assigned to // PsesLaunchRequestArguments.Script, which is then assigned to @@ -317,8 +316,13 @@ public async Task CanLaunchScriptWithCommentedLastLineAsync() ConfigurationDoneResponse configDoneResponse = await PsesDebugAdapterClient.RequestConfigurationDone(new ConfigurationDoneArguments()).ConfigureAwait(true); Assert.NotNull(configDoneResponse); + // We can check that the script was invoked as expected, which is to dot-source a script + // block with the contents surrounded by newlines. While we can't check that the last + // line was a curly brace by itself, we did check that the contents ended with a + // comment, so if this output exists then the bug did not recur. Assert.Collection(await GetLog().ConfigureAwait(true), - (i) => Assert.Equal("a log statement", i)); + (i) => Assert.Equal(". {", i), + (i) => Assert.Equal("", i)); } [SkippableFact] diff --git a/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs b/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs index b58997005..eef37f1ca 100644 --- a/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs +++ b/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs @@ -10,6 +10,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.PowerShell.EditorServices.Handlers; using Microsoft.PowerShell.EditorServices.Services; using Microsoft.PowerShell.EditorServices.Services.DebugAdapter; using Microsoft.PowerShell.EditorServices.Services.PowerShell.Host; @@ -521,6 +522,40 @@ await debugService.SetCommandBreakpointsAsync( Assert.Equal("\"True > \"", prompt.ValueString); } + [SkippableFact] + public async Task DebuggerBreaksInUntitledScript() + { + Skip.IfNot(VersionUtils.PSEdition == "Core", "Untitled script breakpoints only supported in PowerShell Core"); + const string contents = "Write-Output $($MyInvocation.Line)"; + const string scriptPath = "untitled:Untitled-1"; + Assert.True(ScriptFile.IsUntitledPath(scriptPath)); + ScriptFile scriptFile = workspace.GetFileBuffer(scriptPath, contents); + Assert.Equal(scriptFile.DocumentUri, scriptPath); + Assert.Equal(scriptFile.Contents, contents); + Assert.True(workspace.TryGetFile(scriptPath, out ScriptFile _)); + + await debugService.SetCommandBreakpointsAsync( + new[] { CommandBreakpointDetails.Create("Write-Output") }).ConfigureAwait(true); + + ConfigurationDoneHandler configurationDoneHandler = new( + NullLoggerFactory.Instance, null, debugService, null, null, psesHost, workspace, null, psesHost); + + Task _ = configurationDoneHandler.LaunchScriptAsync(scriptPath); + AssertDebuggerStopped(scriptPath, 1); + + VariableDetailsBase[] variables = GetVariables(VariableContainerDetails.CommandVariablesName); + VariableDetailsBase myInvocation = Array.Find(variables, v => v.Name == "$MyInvocation"); + Assert.NotNull(myInvocation); + Assert.True(myInvocation.IsExpandable); + + // Here we're asserting that our hacky workaround to support breakpoints in untitled + // scripts is working, namely that we're actually dot-sourcing our first argument, which + // should be a cached script block. See the `LaunchScriptAsync` for more info. + VariableDetailsBase[] myInvocationChildren = debugService.GetVariables(myInvocation.Id); + VariableDetailsBase myInvocationLine = Array.Find(myInvocationChildren, v => v.Name == "Line"); + Assert.Equal("\". $args[0]\"", myInvocationLine.ValueString); + } + [Fact] public async Task DebuggerVariableStringDisplaysCorrectly() {