Skip to content

Commit a5aaad8

Browse files
JustinGroteSeeminglyScience
andauthoredDec 3, 2024··
Add Breakpoint Label frame to optimize debug stepping performance (#2190)
* Add Breakpoint label frame to optimize debug stepping performance * Incorporate PR Review Feedback * Add Tests Co-authored-by: Patrick Meinecke <SeeminglyScience@users.noreply.github.com>
1 parent 42d1399 commit a5aaad8

File tree

6 files changed

+241
-145
lines changed

6 files changed

+241
-145
lines changed
 

‎src/PowerShellEditorServices/Server/PsesDebugServer.cs

+1
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ public async Task StartAsync()
117117
response.SupportsHitConditionalBreakpoints = true;
118118
response.SupportsLogPoints = true;
119119
response.SupportsSetVariable = true;
120+
response.SupportsDelayedStackTraceLoading = true;
120121

121122
return Task.CompletedTask;
122123
});

‎src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs

+7-58
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@
66
using System.Collections.Generic;
77
using System.Linq;
88
using System.Management.Automation;
9-
using System.Management.Automation.Language;
10-
using System.Reflection;
119
using System.Threading;
1210
using System.Threading.Tasks;
1311
using Microsoft.Extensions.Logging;
@@ -50,7 +48,6 @@ internal class DebugService
5048
private VariableContainerDetails scriptScopeVariables;
5149
private VariableContainerDetails localScopeVariables;
5250
private StackFrameDetails[] stackFrameDetails;
53-
private readonly PropertyInfo invocationTypeScriptPositionProperty;
5451

5552
private readonly SemaphoreSlim debugInfoHandle = AsyncUtils.CreateSimpleLockingSemaphore();
5653
#endregion
@@ -75,6 +72,11 @@ internal class DebugService
7572
/// </summary>
7673
public DebuggerStoppedEventArgs CurrentDebuggerStoppedEventArgs { get; private set; }
7774

75+
/// <summary>
76+
/// Returns a task that completes when script frames and variables have completed population
77+
/// </summary>
78+
public Task StackFramesAndVariablesFetched { get; private set; }
79+
7880
/// <summary>
7981
/// Tracks whether we are running <c>Debug-Runspace</c> in an out-of-process runspace.
8082
/// </summary>
@@ -111,12 +113,6 @@ public DebugService(
111113
_debugContext.DebuggerResuming += OnDebuggerResuming;
112114
_debugContext.BreakpointUpdated += OnBreakpointUpdated;
113115
_remoteFileManager = remoteFileManager;
114-
115-
invocationTypeScriptPositionProperty =
116-
typeof(InvocationInfo)
117-
.GetProperty(
118-
"ScriptPosition",
119-
BindingFlags.NonPublic | BindingFlags.Instance);
120116
}
121117

122118
#endregion
@@ -981,8 +977,8 @@ await _executionService.ExecutePSCommandAsync<PSObject>(
981977
}
982978
}
983979

984-
// Get call stack and variables.
985-
await FetchStackFramesAndVariablesAsync(noScriptName ? localScriptPath : null).ConfigureAwait(false);
980+
// Begin call stack and variables fetch. We don't need to block here.
981+
StackFramesAndVariablesFetched = FetchStackFramesAndVariablesAsync(noScriptName ? localScriptPath : null);
986982

987983
// If this is a remote connection and the debugger stopped at a line
988984
// in a script file, get the file contents
@@ -996,53 +992,6 @@ await _remoteFileManager.FetchRemoteFileAsync(
996992
_psesHost.CurrentRunspace).ConfigureAwait(false);
997993
}
998994

999-
if (stackFrameDetails.Length > 0)
1000-
{
1001-
// Augment the top stack frame with details from the stop event
1002-
if (invocationTypeScriptPositionProperty.GetValue(e.InvocationInfo) is IScriptExtent scriptExtent)
1003-
{
1004-
StackFrameDetails targetFrame = stackFrameDetails[0];
1005-
1006-
// Certain context changes (like stepping into the default value expression
1007-
// of a parameter) do not create a call stack frame. In order to represent
1008-
// this context change we create a fake call stack frame.
1009-
if (!string.IsNullOrEmpty(scriptExtent.File)
1010-
&& !PathUtils.IsPathEqual(scriptExtent.File, targetFrame.ScriptPath))
1011-
{
1012-
await debugInfoHandle.WaitAsync().ConfigureAwait(false);
1013-
try
1014-
{
1015-
targetFrame = new StackFrameDetails
1016-
{
1017-
ScriptPath = scriptExtent.File,
1018-
// Just use the last frame's variables since we don't have a
1019-
// good way to get real values.
1020-
AutoVariables = targetFrame.AutoVariables,
1021-
CommandVariables = targetFrame.CommandVariables,
1022-
// Ideally we'd get a real value here but since there's no real
1023-
// call stack frame for this, we'd need to replicate a lot of
1024-
// engine code.
1025-
FunctionName = "<ScriptBlock>",
1026-
};
1027-
1028-
StackFrameDetails[] newFrames = new StackFrameDetails[stackFrameDetails.Length + 1];
1029-
newFrames[0] = targetFrame;
1030-
stackFrameDetails.CopyTo(newFrames, 1);
1031-
stackFrameDetails = newFrames;
1032-
}
1033-
finally
1034-
{
1035-
debugInfoHandle.Release();
1036-
}
1037-
}
1038-
1039-
targetFrame.StartLineNumber = scriptExtent.StartLineNumber;
1040-
targetFrame.EndLineNumber = scriptExtent.EndLineNumber;
1041-
targetFrame.StartColumnNumber = scriptExtent.StartColumnNumber;
1042-
targetFrame.EndColumnNumber = scriptExtent.EndColumnNumber;
1043-
}
1044-
}
1045-
1046995
CurrentDebuggerStoppedEventArgs =
1047996
new DebuggerStoppedEventArgs(
1048997
e,

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

+16-5
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,27 @@ 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-
VariableScope[] variableScopes =
24-
_debugService.GetVariableScopes(
25-
(int)request.FrameId);
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;
33+
34+
VariableScope[] variableScopes = _debugService.GetVariableScopes(powershellFrameId);
2635

2736
return Task.FromResult(new ScopesResponse
2837
{
29-
Scopes = new Container<Scope>(variableScopes
30-
.Select(LspDebugUtils.CreateScope))
38+
Scopes = new Container<Scope>(
39+
variableScopes
40+
.Select(LspDebugUtils.CreateScope)
41+
)
3142
});
3243
}
3344
}
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,133 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
3+
#nullable enable
34

45
using System;
56
using System.Collections.Generic;
67
using System.Threading;
78
using System.Threading.Tasks;
9+
using System.Management.Automation;
810
using Microsoft.PowerShell.EditorServices.Services;
9-
using Microsoft.PowerShell.EditorServices.Services.DebugAdapter;
10-
using Microsoft.PowerShell.EditorServices.Utility;
1111
using OmniSharp.Extensions.DebugAdapter.Protocol.Models;
1212
using OmniSharp.Extensions.DebugAdapter.Protocol.Requests;
13+
using Microsoft.PowerShell.EditorServices.Services.DebugAdapter;
14+
using System.Linq;
15+
using OmniSharp.Extensions.JsonRpc;
16+
17+
namespace Microsoft.PowerShell.EditorServices.Handlers;
1318

14-
namespace Microsoft.PowerShell.EditorServices.Handlers
19+
internal class StackTraceHandler(DebugService debugService) : IStackTraceHandler
1520
{
16-
internal class StackTraceHandler : IStackTraceHandler
21+
/// <summary>
22+
/// Because we don't know the size of the stacktrace beforehand, we will tell the client that there are more frames available, this is effectively a paging size, as the client should request this many frames after the first one.
23+
/// </summary>
24+
private const int INITIAL_PAGE_SIZE = 20;
25+
26+
public async Task<StackTraceResponse> Handle(StackTraceArguments request, CancellationToken cancellationToken)
1727
{
18-
private readonly DebugService _debugService;
28+
if (!debugService.IsDebuggerStopped)
29+
{
30+
throw new RpcErrorException(0, null!, "Stacktrace was requested while we are not stopped at a breakpoint. This is a violation of the DAP protocol, and is probably a bug.");
31+
}
1932

20-
public StackTraceHandler(DebugService debugService) => _debugService = debugService;
33+
// Adapting to int to let us use LINQ, realistically if you have a stacktrace larger than this that the client is requesting, you have bigger problems...
34+
int skip = Convert.ToInt32(request.StartFrame ?? 0);
35+
int take = Convert.ToInt32(request.Levels ?? 0);
2136

22-
public async Task<StackTraceResponse> Handle(StackTraceArguments request, CancellationToken cancellationToken)
23-
{
24-
StackFrameDetails[] stackFrameDetails = await _debugService.GetStackFramesAsync(cancellationToken).ConfigureAwait(false);
37+
// We generate a label for the breakpoint and can return that immediately if the client is supporting DelayedStackTraceLoading.
38+
InvocationInfo invocationInfo = debugService.CurrentDebuggerStoppedEventArgs?.OriginalEvent?.InvocationInfo
39+
?? throw new RpcErrorException(0, null!, "InvocationInfo was not available on CurrentDebuggerStoppedEvent args. This is a bug and you should report it.");
2540

26-
// Handle a rare race condition where the adapter requests stack frames before they've
27-
// begun building.
28-
if (stackFrameDetails is null)
29-
{
30-
return new StackTraceResponse
31-
{
32-
StackFrames = Array.Empty<StackFrame>(),
33-
TotalFrames = 0
34-
};
35-
}
36-
37-
List<StackFrame> newStackFrames = new();
38-
39-
long startFrameIndex = request.StartFrame ?? 0;
40-
long maxFrameCount = stackFrameDetails.Length;
41-
42-
// If the number of requested levels == 0 (or null), that means get all stack frames
43-
// after the specified startFrame index. Otherwise get all the stack frames.
44-
long requestedFrameCount = request.Levels ?? 0;
45-
if (requestedFrameCount > 0)
46-
{
47-
maxFrameCount = Math.Min(maxFrameCount, startFrameIndex + requestedFrameCount);
48-
}
41+
StackFrame breakpointLabel = CreateBreakpointLabel(invocationInfo);
4942

50-
for (long i = startFrameIndex; i < maxFrameCount; i++)
43+
if (skip == 0 && take == 1) // This indicates the client is doing an initial fetch, so we want to return quickly to unblock the UI and wait on the remaining stack frames for the subsequent requests.
44+
{
45+
return new StackTraceResponse()
5146
{
52-
// Create the new StackFrame object with an ID that can
53-
// be referenced back to the current list of stack frames
54-
newStackFrames.Add(LspDebugUtils.CreateStackFrame(stackFrameDetails[i], id: i));
55-
}
47+
StackFrames = new StackFrame[] { breakpointLabel },
48+
TotalFrames = INITIAL_PAGE_SIZE //Indicate to the client that there are more frames available
49+
};
50+
}
51+
52+
// Wait until the stack frames and variables have been fetched.
53+
await debugService.StackFramesAndVariablesFetched.ConfigureAwait(false);
54+
55+
StackFrameDetails[] stackFrameDetails = await debugService.GetStackFramesAsync(cancellationToken)
56+
.ConfigureAwait(false);
5657

58+
// Handle a rare race condition where the adapter requests stack frames before they've
59+
// begun building.
60+
if (stackFrameDetails is null)
61+
{
5762
return new StackTraceResponse
5863
{
59-
StackFrames = newStackFrames,
60-
TotalFrames = newStackFrames.Count
64+
StackFrames = Array.Empty<StackFrame>(),
65+
TotalFrames = 0
66+
};
67+
}
68+
69+
List<StackFrame> newStackFrames = new();
70+
if (skip == 0)
71+
{
72+
newStackFrames.Add(breakpointLabel);
73+
}
74+
75+
newStackFrames.AddRange(
76+
stackFrameDetails
77+
.Skip(skip != 0 ? skip - 1 : skip)
78+
.Take(take != 0 ? take - 1 : take)
79+
.Select((frame, index) => CreateStackFrame(frame, index + 1))
80+
);
81+
82+
return new StackTraceResponse
83+
{
84+
StackFrames = newStackFrames,
85+
TotalFrames = newStackFrames.Count
86+
};
87+
}
88+
89+
public static StackFrame CreateStackFrame(StackFrameDetails stackFrame, long id)
90+
{
91+
SourcePresentationHint sourcePresentationHint =
92+
stackFrame.IsExternalCode ? SourcePresentationHint.Deemphasize : SourcePresentationHint.Normal;
93+
94+
// When debugging an interactive session, the ScriptPath is <No File> which is not a valid source file.
95+
// We need to make sure the user can't open the file associated with this stack frame.
96+
// It will generate a VSCode error in this case.
97+
Source? source = null;
98+
if (!stackFrame.ScriptPath.Contains("<No File>"))
99+
{
100+
source = new Source
101+
{
102+
Path = stackFrame.ScriptPath,
103+
PresentationHint = sourcePresentationHint
61104
};
62105
}
106+
107+
return new StackFrame
108+
{
109+
Id = id,
110+
Name = (source is not null) ? stackFrame.FunctionName : "Interactive Session",
111+
Line = (source is not null) ? stackFrame.StartLineNumber : 0,
112+
EndLine = stackFrame.EndLineNumber,
113+
Column = (source is not null) ? stackFrame.StartColumnNumber : 0,
114+
EndColumn = stackFrame.EndColumnNumber,
115+
Source = source
116+
};
63117
}
118+
119+
public static StackFrame CreateBreakpointLabel(InvocationInfo invocationInfo, int id = 0) => new()
120+
{
121+
Name = "<Breakpoint>",
122+
Id = id,
123+
Source = new()
124+
{
125+
Path = invocationInfo.ScriptName
126+
},
127+
Line = invocationInfo.ScriptLineNumber,
128+
Column = invocationInfo.OffsetInLine,
129+
PresentationHint = StackFramePresentationHint.Label
130+
};
131+
64132
}
133+

‎src/PowerShellEditorServices/Utility/LspDebugUtils.cs

-32
Original file line numberDiff line numberDiff line change
@@ -56,38 +56,6 @@ public static Breakpoint CreateBreakpoint(
5656
};
5757
}
5858

59-
public static StackFrame CreateStackFrame(
60-
StackFrameDetails stackFrame,
61-
long id)
62-
{
63-
SourcePresentationHint sourcePresentationHint =
64-
stackFrame.IsExternalCode ? SourcePresentationHint.Deemphasize : SourcePresentationHint.Normal;
65-
66-
// When debugging an interactive session, the ScriptPath is <No File> which is not a valid source file.
67-
// We need to make sure the user can't open the file associated with this stack frame.
68-
// It will generate a VSCode error in this case.
69-
Source source = null;
70-
if (!stackFrame.ScriptPath.Contains("<"))
71-
{
72-
source = new Source
73-
{
74-
Path = stackFrame.ScriptPath,
75-
PresentationHint = sourcePresentationHint
76-
};
77-
}
78-
79-
return new StackFrame
80-
{
81-
Id = id,
82-
Name = (source != null) ? stackFrame.FunctionName : "Interactive Session",
83-
Line = (source != null) ? stackFrame.StartLineNumber : 0,
84-
EndLine = stackFrame.EndLineNumber,
85-
Column = (source != null) ? stackFrame.StartColumnNumber : 0,
86-
EndColumn = stackFrame.EndColumnNumber,
87-
Source = source
88-
};
89-
}
90-
9159
public static Scope CreateScope(VariableScope scope)
9260
{
9361
return new Scope

‎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.