Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 414599f

Browse files
committedDec 2, 2024·
Add Tests
1 parent b8b3aa2 commit 414599f

File tree

2 files changed

+120
-14
lines changed

2 files changed

+120
-14
lines changed
 

‎src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ScopesHandler.cs

+11-3
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,20 @@ internal class ScopesHandler : IScopesHandler
1818

1919
public ScopesHandler(DebugService debugService) => _debugService = debugService;
2020

21+
/// <summary>
22+
/// Retrieves the variable scopes (containers) for the currently selected stack frame. Variables details are fetched via a separate request.
23+
/// </summary>
2124
public Task<ScopesResponse> Handle(ScopesArguments request, CancellationToken cancellationToken)
2225
{
23-
//We have an artificial breakpoint label, so just copy the stacktrace from the first stack entry for this.
24-
int frameId = request.FrameId == 0 ? 0 : (int)request.FrameId - 1;
26+
// HACK: The StackTraceHandler injects an artificial label frame as the first frame as a performance optimization, so when scopes are requested by the client, we need to adjust the frame index accordingly to match the underlying PowerShell frame, so when the client clicks on the label (or hit the default breakpoint), they get variables populated from the top of the PowerShell stackframe. If the client dives deeper, we need to reflect that as well (though 90% of debug users don't actually investigate this)
27+
// VSCode Frame 0 (Label) -> PowerShell StackFrame 0 (for convenience)
28+
// VSCode Frame 1 (First Real PS Frame) -> Also PowerShell StackFrame 0
29+
// VSCode Frame 2 -> PowerShell StackFrame 1
30+
// VSCode Frame 3 -> PowerShell StackFrame 2
31+
// etc.
32+
int powershellFrameId = request.FrameId == 0 ? 0 : (int)request.FrameId - 1;
2533

26-
VariableScope[] variableScopes = _debugService.GetVariableScopes(frameId);
34+
VariableScope[] variableScopes = _debugService.GetVariableScopes(powershellFrameId);
2735

2836
return Task.FromResult(new ScopesResponse
2937
{

‎test/PowerShellEditorServices.Test.E2E/DebugAdapterProtocolMessageTests.cs

+109-11
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,31 @@
22
// Licensed under the MIT License.
33

44
using System;
5+
using System.Diagnostics;
56
using System.IO;
67
using System.Linq;
78
using System.Runtime.InteropServices;
89
using System.Text;
910
using System.Threading;
1011
using System.Threading.Tasks;
11-
using Microsoft.Extensions.Logging;
12-
using Microsoft.Extensions.Logging.Debug;
1312
using OmniSharp.Extensions.DebugAdapter.Client;
13+
using DapStackFrame = OmniSharp.Extensions.DebugAdapter.Protocol.Models.StackFrame;
14+
using OmniSharp.Extensions.DebugAdapter.Protocol.Events;
1415
using OmniSharp.Extensions.DebugAdapter.Protocol.Models;
1516
using OmniSharp.Extensions.DebugAdapter.Protocol.Requests;
17+
using OmniSharp.Extensions.JsonRpc.Server;
1618
using Xunit;
1719
using Xunit.Abstractions;
20+
using Microsoft.Extensions.Logging.Abstractions;
1821

1922
namespace PowerShellEditorServices.Test.E2E
2023
{
24+
public class XunitOutputTraceListener(ITestOutputHelper output) : TraceListener
25+
{
26+
public override void Write(string message) => output.WriteLine(message);
27+
public override void WriteLine(string message) => output.WriteLine(message);
28+
}
29+
2130
[Trait("Category", "DAP")]
2231
public class DebugAdapterProtocolMessageTests : IAsyncLifetime, IDisposable
2332
{
@@ -28,16 +37,26 @@ public class DebugAdapterProtocolMessageTests : IAsyncLifetime, IDisposable
2837
private DebugAdapterClient PsesDebugAdapterClient;
2938
private PsesStdioProcess _psesProcess;
3039

40+
/// <summary>
41+
/// Completes when the debug adapter is started.
42+
/// </summary>
3143
public TaskCompletionSource<object> Started { get; } = new TaskCompletionSource<object>();
32-
44+
/// <summary>
45+
/// Completes when the first breakpoint is reached.
46+
/// </summary>
47+
public TaskCompletionSource<StoppedEvent> Stopped { get; } = new TaskCompletionSource<StoppedEvent>();
48+
49+
/// <summary>
50+
/// Constructor. The ITestOutputHelper is injected by xUnit and used to write diagnostic logs.
51+
/// </summary>
52+
/// <param name="output"></param>
3353
public DebugAdapterProtocolMessageTests(ITestOutputHelper output) => _output = output;
3454

3555
public async Task InitializeAsync()
3656
{
37-
LoggerFactory debugLoggerFactory = new();
38-
debugLoggerFactory.AddProvider(new DebugLoggerProvider());
57+
// NOTE: To see debug logger output, add this line to your test
3958

40-
_psesProcess = new PsesStdioProcess(debugLoggerFactory, true);
59+
_psesProcess = new PsesStdioProcess(new NullLoggerFactory(), true);
4160
await _psesProcess.Start();
4261

4362
TaskCompletionSource<bool> initialized = new();
@@ -53,18 +72,20 @@ public async Task InitializeAsync()
5372
options
5473
.WithInput(_psesProcess.OutputStream)
5574
.WithOutput(_psesProcess.InputStream)
56-
.ConfigureLogging(builder =>
57-
builder
58-
.AddDebug()
59-
.SetMinimumLevel(LogLevel.Trace)
60-
)
6175
// The OnStarted delegate gets run when we receive the _Initialized_ event from the server:
6276
// https://microsoft.github.io/debug-adapter-protocol/specification#Events_Initialized
6377
.OnStarted((_, _) =>
6478
{
6579
Started.SetResult(true);
6680
return Task.CompletedTask;
6781
})
82+
// We use this to create a task we can await to test debugging after a breakpoint has been received.
83+
.OnNotification<StoppedEvent>(null, (stoppedEvent, _) =>
84+
{
85+
Console.WriteLine("StoppedEvent received");
86+
Stopped.SetResult(stoppedEvent);
87+
return Task.CompletedTask;
88+
})
6889
// The OnInitialized delegate gets run when we first receive the _Initialize_ response:
6990
// https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Initialize
7091
.OnInitialized((_, _, _, _) =>
@@ -263,6 +284,83 @@ public async Task CanSetBreakpointsAsync()
263284
(i) => Assert.Equal("after breakpoint", i));
264285
}
265286

287+
[SkippableFact]
288+
public async Task FailsIfStacktraceRequestedWhenNotPaused()
289+
{
290+
Skip.If(PsesStdioProcess.RunningInConstrainedLanguageMode,
291+
"Breakpoints can't be set in Constrained Language Mode.");
292+
string filePath = NewTestFile(GenerateScriptFromLoggingStatements(
293+
"labelTestBreakpoint"
294+
));
295+
// Set a breakpoint
296+
await PsesDebugAdapterClient.SetBreakpoints(
297+
new SetBreakpointsArguments
298+
{
299+
Source = new Source { Name = Path.GetFileName(filePath), Path = filePath },
300+
Breakpoints = new SourceBreakpoint[] { new SourceBreakpoint { Line = 1 } },
301+
SourceModified = false,
302+
}
303+
);
304+
305+
// Signal to start the script
306+
await PsesDebugAdapterClient.RequestConfigurationDone(new ConfigurationDoneArguments());
307+
await PsesDebugAdapterClient.LaunchScript(filePath, Started);
308+
309+
310+
// Get the stacktrace for the breakpoint
311+
await Assert.ThrowsAsync<JsonRpcException>(() => PsesDebugAdapterClient.RequestStackTrace(
312+
new StackTraceArguments { }
313+
));
314+
}
315+
316+
[SkippableFact]
317+
public async Task SendsInitialLabelBreakpointForPerformanceReasons()
318+
{
319+
Skip.If(PsesStdioProcess.RunningInConstrainedLanguageMode,
320+
"Breakpoints can't be set in Constrained Language Mode.");
321+
string filePath = NewTestFile(GenerateScriptFromLoggingStatements(
322+
"before breakpoint",
323+
"at breakpoint",
324+
"after breakpoint"
325+
));
326+
327+
//TODO: This is technically wrong per the spec, configDone should be completed BEFORE launching, but this is how the vscode client does it today and we really need to fix that.
328+
await PsesDebugAdapterClient.LaunchScript(filePath, Started);
329+
330+
// {"command":"setBreakpoints","arguments":{"source":{"name":"dfsdfg.ps1","path":"/Users/tyleonha/Code/PowerShell/Misc/foo/dfsdfg.ps1"},"lines":[2],"breakpoints":[{"line":2}],"sourceModified":false},"type":"request","seq":3}
331+
SetBreakpointsResponse setBreakpointsResponse = await PsesDebugAdapterClient.SetBreakpoints(new SetBreakpointsArguments
332+
{
333+
Source = new Source { Name = Path.GetFileName(filePath), Path = filePath },
334+
Breakpoints = new SourceBreakpoint[] { new SourceBreakpoint { Line = 2 } },
335+
SourceModified = false,
336+
});
337+
338+
Breakpoint breakpoint = setBreakpointsResponse.Breakpoints.First();
339+
Assert.True(breakpoint.Verified);
340+
Assert.Equal(filePath, breakpoint.Source.Path, ignoreCase: s_isWindows);
341+
Assert.Equal(2, breakpoint.Line);
342+
343+
ConfigurationDoneResponse configDoneResponse = await PsesDebugAdapterClient.RequestConfigurationDone(new ConfigurationDoneArguments());
344+
345+
// FIXME: I think there is a race condition here. If you remove this, the following line Stack Trace fails because the breakpoint hasn't been hit yet. I think the whole getLog process just works long enough for ConfigurationDone to complete and for the breakpoint to be hit.
346+
347+
// I've tried to do this properly by waiting for a StoppedEvent, but that doesn't seem to work, I'm probably just not wiring it up right in the handler.
348+
Assert.NotNull(configDoneResponse);
349+
Assert.Collection(await GetLog(),
350+
(i) => Assert.Equal("before breakpoint", i));
351+
File.Delete(s_testOutputPath);
352+
353+
// Get the stacktrace for the breakpoint
354+
StackTraceResponse stackTraceResponse = await PsesDebugAdapterClient.RequestStackTrace(
355+
new StackTraceArguments { ThreadId = 1 }
356+
);
357+
DapStackFrame firstFrame = stackTraceResponse.StackFrames.First();
358+
Assert.Equal(
359+
firstFrame.PresentationHint,
360+
StackFramePresentationHint.Label
361+
);
362+
}
363+
266364
// This is a regression test for a bug where user code causes a new synchronization context
267365
// to be created, breaking the extension. It's most evident when debugging PowerShell
268366
// scripts that use System.Windows.Forms. It required fixing both Editor Services and

0 commit comments

Comments
 (0)
Please sign in to comment.