Skip to content

Commit 7b53348

Browse files
committed
Replaced file wait with async log retrieval
1 parent 42b5fea commit 7b53348

File tree

1 file changed

+108
-51
lines changed

1 file changed

+108
-51
lines changed

test/PowerShellEditorServices.Test.E2E/DebugAdapterProtocolMessageTests.cs

+108-51
Original file line numberDiff line numberDiff line change
@@ -30,21 +30,38 @@ public class DebugAdapterProtocolMessageTests(ITestOutputHelper output) : IAsync
3030
private IDebugAdapterClient client;
3131

3232
private static readonly bool s_isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
33-
private static readonly string s_testTempPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
33+
34+
/// <summary>
35+
/// Test scripts output here, where the output can be read to verify script progress against breakpointing
36+
/// </summary>
37+
private static readonly string testScriptLogPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
3438

3539
private readonly PsesStdioLanguageServerProcessHost psesHost = new(isDebugAdapter: true);
3640

3741
private readonly TaskCompletionSource<IDebugAdapterClient> initializedLanguageClientTcs = new();
42+
/// <summary>
43+
/// This task is useful for waiting until the client is initialized (but before Server Initialized is sent)
44+
/// </summary>
3845
private Task<IDebugAdapterClient> initializedLanguageClient => initializedLanguageClientTcs.Task;
3946

4047
/// <summary>
41-
/// This TCS is useful for waiting until a breakpoint is hit in a test.
42-
/// </summary>
48+
/// Is used to read the script log file to verify script progress against breakpointing.
49+
private StreamReader scriptLogReader;
50+
4351
private TaskCompletionSource<StoppedEvent> nextStoppedTcs = new();
52+
/// <summary>
53+
/// This task is useful for waiting until a breakpoint is hit in a test.
54+
/// </summary>
4455
private Task<StoppedEvent> nextStopped => nextStoppedTcs.Task;
4556

4657
public async Task InitializeAsync()
4758
{
59+
// Cleanup testScriptLogPath if it exists due to an interrupted previous run
60+
if (File.Exists(testScriptLogPath))
61+
{
62+
File.Delete(testScriptLogPath);
63+
}
64+
4865
(StreamReader stdout, StreamWriter stdin) = await psesHost.Start();
4966

5067
// Splice the streams together and enable debug logging of all messages sent and received
@@ -106,6 +123,12 @@ await client.RequestDisconnect(new DisconnectArguments
106123
});
107124
client?.Dispose();
108125
psesHost.Stop();
126+
127+
scriptLogReader?.Dispose(); //Also disposes the underlying filestream
128+
if (File.Exists(testScriptLogPath))
129+
{
130+
File.Delete(testScriptLogPath);
131+
}
109132
}
110133

111134
private static string NewTestFile(string script, bool isPester = false)
@@ -117,17 +140,24 @@ private static string NewTestFile(string script, bool isPester = false)
117140
return filePath;
118141
}
119142

120-
private string GenerateScriptFromLoggingStatements(params string[] logStatements)
143+
/// <summary>
144+
/// Given an array of strings, generate a PowerShell script that writes each string to our test script log path
145+
/// so it can be read back later to verify script progress against breakpointing.
146+
/// </summary>
147+
/// <param name="logStatements">A list of statements that for which a script will be generated to write each statement to a testing log that can be read by <see cref="ReadScriptLogLineAsync" />. The strings are double quoted in Powershell, so variables such as <c>$($PSScriptRoot)</c> etc. can be used</param>
148+
/// <returns>A script string that should be written to disk and instructed by PSES to execute</returns>
149+
/// <exception cref="ArgumentNullException"></exception>
150+
private string GenerateLoggingScript(params string[] logStatements)
121151
{
122152
if (logStatements.Length == 0)
123153
{
124154
throw new ArgumentNullException(nameof(logStatements), "Expected at least one argument.");
125155
}
126156

127157
// Clean up side effects from other test runs.
128-
if (File.Exists(s_testTempPath))
158+
if (File.Exists(testScriptLogPath))
129159
{
130-
File.Delete(s_testTempPath);
160+
File.Delete(testScriptLogPath);
131161
}
132162

133163
// Have script create file first with `>` (but don't rely on overwriting).
@@ -136,7 +166,7 @@ private string GenerateScriptFromLoggingStatements(params string[] logStatements
136166
.Append("Write-Output \"")
137167
.Append(logStatements[0])
138168
.Append("\" > '")
139-
.Append(s_testTempPath)
169+
.Append(testScriptLogPath)
140170
.AppendLine("'");
141171

142172
for (int i = 1; i < logStatements.Length; i++)
@@ -146,7 +176,7 @@ private string GenerateScriptFromLoggingStatements(params string[] logStatements
146176
.Append("Write-Output \"")
147177
.Append(logStatements[i])
148178
.Append("\" >> '")
149-
.Append(s_testTempPath)
179+
.Append(testScriptLogPath)
150180
.AppendLine("'");
151181
}
152182

@@ -155,15 +185,37 @@ private string GenerateScriptFromLoggingStatements(params string[] logStatements
155185
return builder.ToString();
156186
}
157187

158-
private static async Task<string[]> GetLog()
188+
/// <summary>
189+
/// Reads the next output line from the test script log file. Useful in assertions to verify script progress against breakpointing.
190+
/// </summary>
191+
private async Task<string> ReadScriptLogLineAsync()
159192
{
160-
for (int i = 0; !File.Exists(s_testTempPath) && i < 60; i++)
193+
while (scriptLogReader is null)
161194
{
162-
await Task.Delay(1000);
195+
try
196+
{
197+
scriptLogReader = new StreamReader(
198+
new FileStream(
199+
testScriptLogPath,
200+
FileMode.OpenOrCreate,
201+
FileAccess.Read, // Because we use append, its OK to create the file ahead of the script
202+
FileShare.ReadWrite
203+
)
204+
);
205+
}
206+
catch (IOException) //Sadly there does not appear to be a xplat way to wait for file availability, but luckily this does not appear to fire often.
207+
{
208+
await Task.Delay(500);
209+
}
210+
}
211+
212+
// return valid lines only
213+
string nextLine = string.Empty;
214+
while (nextLine is null || nextLine.Length == 0)
215+
{
216+
nextLine = await scriptLogReader.ReadLineAsync(); //Might return null if at EOF because we created it above but the script hasn't written to it yet
163217
}
164-
// Sleep one more time after the file exists so whatever is writing can finish.
165-
await Task.Delay(1000);
166-
return File.ReadLines(s_testTempPath).ToArray();
218+
return nextLine;
167219
}
168220

169221
[Fact]
@@ -181,36 +233,35 @@ public void CanInitializeWithCorrectServerSettings()
181233
[Fact]
182234
public async Task UsesDotSourceOperatorAndQuotesAsync()
183235
{
184-
string filePath = NewTestFile(GenerateScriptFromLoggingStatements("$($MyInvocation.Line)"));
236+
string filePath = NewTestFile(GenerateLoggingScript("$($MyInvocation.Line)"));
185237
await client.LaunchScript(filePath);
186238
ConfigurationDoneResponse configDoneResponse = await client.RequestConfigurationDone(new ConfigurationDoneArguments());
187239
Assert.NotNull(configDoneResponse);
188-
Assert.Collection(await GetLog(),
189-
(i) => Assert.StartsWith(". '", i));
240+
string actual = await ReadScriptLogLineAsync();
241+
Assert.StartsWith(". '", actual);
190242
}
191243

192244
[Fact]
193245
public async Task UsesCallOperatorWithSettingAsync()
194246
{
195-
string filePath = NewTestFile(GenerateScriptFromLoggingStatements("$($MyInvocation.Line)"));
247+
string filePath = NewTestFile(GenerateLoggingScript("$($MyInvocation.Line)"));
196248
await client.LaunchScript(filePath, executeMode: "Call");
197249
ConfigurationDoneResponse configDoneResponse = await client.RequestConfigurationDone(new ConfigurationDoneArguments());
198250
Assert.NotNull(configDoneResponse);
199-
Assert.Collection(await GetLog(),
200-
(i) => Assert.StartsWith("& '", i));
251+
string actual = await ReadScriptLogLineAsync();
252+
Assert.StartsWith("& '", actual);
201253
}
202254

203255
[Fact]
204256
public async Task CanLaunchScriptWithNoBreakpointsAsync()
205257
{
206-
string filePath = NewTestFile(GenerateScriptFromLoggingStatements("works"));
258+
string filePath = NewTestFile(GenerateLoggingScript("works"));
207259

208260
await client.LaunchScript(filePath);
209261

210262
ConfigurationDoneResponse configDoneResponse = await client.RequestConfigurationDone(new ConfigurationDoneArguments());
211263
Assert.NotNull(configDoneResponse);
212-
Assert.Collection(await GetLog(),
213-
(i) => Assert.Equal("works", i));
264+
Assert.Equal("works", await ReadScriptLogLineAsync());
214265
}
215266

216267
[SkippableFact]
@@ -219,7 +270,7 @@ public async Task CanSetBreakpointsAsync()
219270
Skip.If(PsesStdioLanguageServerProcessHost.RunningInConstrainedLanguageMode,
220271
"Breakpoints can't be set in Constrained Language Mode.");
221272

222-
string filePath = NewTestFile(GenerateScriptFromLoggingStatements(
273+
string filePath = NewTestFile(GenerateLoggingScript(
223274
"before breakpoint",
224275
"at breakpoint",
225276
"after breakpoint"
@@ -243,32 +294,36 @@ public async Task CanSetBreakpointsAsync()
243294
ConfigurationDoneResponse configDoneResponse = await client.RequestConfigurationDone(new ConfigurationDoneArguments());
244295
Assert.NotNull(configDoneResponse);
245296

297+
// Wait until we hit the breakpoint
246298
StoppedEvent stoppedEvent = await nextStopped;
299+
Assert.Equal("breakpoint", stoppedEvent.Reason);
300+
301+
// The code before the breakpoint should have already run
302+
Assert.Equal("before breakpoint", await ReadScriptLogLineAsync());
247303

248304
// Assert that the stopped breakpoint is the one we set
249305
StackTraceResponse stackTraceResponse = await client.RequestStackTrace(new StackTraceArguments { ThreadId = 1 });
250306
DapStackFrame stoppedTopFrame = stackTraceResponse.StackFrames.First();
251307
Assert.Equal(2, stoppedTopFrame.Line);
252308

309+
_ = await client.RequestContinue(new ContinueArguments { ThreadId = 1 });
253310

254-
ContinueResponse continueResponse = await client.RequestContinue(
255-
new ContinueArguments { ThreadId = 1 });
256-
257-
Assert.NotNull(continueResponse);
258-
Assert.Collection(await GetLog(),
259-
(i) => Assert.Equal("at breakpoint", i),
260-
(i) => Assert.Equal("after breakpoint", i));
311+
Assert.Equal("at breakpoint", await ReadScriptLogLineAsync());
312+
Assert.Equal("after breakpoint", await ReadScriptLogLineAsync());
261313
}
262314

263315
[SkippableFact]
264316
public async Task FailsIfStacktraceRequestedWhenNotPaused()
265317
{
266318
Skip.If(PsesStdioLanguageServerProcessHost.RunningInConstrainedLanguageMode,
267319
"Breakpoints can't be set in Constrained Language Mode.");
268-
string filePath = NewTestFile(GenerateScriptFromLoggingStatements(
269-
"labelTestBreakpoint"
320+
321+
// We want a long running script that never hits the next breakpoint
322+
string filePath = NewTestFile(GenerateLoggingScript(
323+
"$(sleep 10)",
324+
"Should fail before we get here"
270325
));
271-
// Set a breakpoint
326+
272327
await client.SetBreakpoints(
273328
new SetBreakpointsArguments
274329
{
@@ -282,7 +337,7 @@ await client.SetBreakpoints(
282337
await client.RequestConfigurationDone(new ConfigurationDoneArguments());
283338
await client.LaunchScript(filePath);
284339

285-
// Get the stacktrace for the breakpoint
340+
// Try to get the stacktrace. If we are not at a breakpoint, this should fail.
286341
await Assert.ThrowsAsync<JsonRpcException>(() => client.RequestStackTrace(
287342
new StackTraceArguments { }
288343
));
@@ -293,17 +348,14 @@ public async Task SendsInitialLabelBreakpointForPerformanceReasons()
293348
{
294349
Skip.If(PsesStdioLanguageServerProcessHost.RunningInConstrainedLanguageMode,
295350
"Breakpoints can't be set in Constrained Language Mode.");
296-
string filePath = NewTestFile(GenerateScriptFromLoggingStatements(
351+
string filePath = NewTestFile(GenerateLoggingScript(
297352
"before breakpoint",
298-
"at breakpoint",
299-
"after breakpoint"
353+
"label breakpoint"
300354
));
301355

302-
//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.
356+
// Trigger a launch. Note that per DAP spec, launch doesn't actually begin until ConfigDone finishes.
303357
await client.LaunchScript(filePath);
304358

305-
306-
// {"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}
307359
SetBreakpointsResponse setBreakpointsResponse = await client.SetBreakpoints(new SetBreakpointsArguments
308360
{
309361
Source = new Source { Name = Path.GetFileName(filePath), Path = filePath },
@@ -318,16 +370,20 @@ public async Task SendsInitialLabelBreakpointForPerformanceReasons()
318370

319371
_ = client.RequestConfigurationDone(new ConfigurationDoneArguments());
320372

321-
// Resume when the next breakpoint hits
373+
// Wait for the breakpoint to be hit
322374
StoppedEvent stoppedEvent = await nextStopped;
323-
324375
Assert.Equal("breakpoint", stoppedEvent.Reason);
325376

377+
// The code before the breakpoint should have already run
378+
Assert.Equal("before breakpoint", await ReadScriptLogLineAsync());
379+
326380
// Get the stacktrace for the breakpoint
327381
StackTraceResponse stackTraceResponse = await client.RequestStackTrace(
328382
new StackTraceArguments { ThreadId = 1 }
329383
);
330384
DapStackFrame firstFrame = stackTraceResponse.StackFrames.First();
385+
386+
// Our synthetic label breakpoint should be present
331387
Assert.Equal(
332388
StackFramePresentationHint.Label,
333389
firstFrame.PresentationHint
@@ -391,7 +447,7 @@ public async Task CanStepPastSystemWindowsForms()
391447
[Fact]
392448
public async Task CanLaunchScriptWithCommentedLastLineAsync()
393449
{
394-
string script = GenerateScriptFromLoggingStatements("$($MyInvocation.Line)") + "# a comment at the end";
450+
string script = GenerateLoggingScript("$($MyInvocation.Line)", "$(1+1)") + "# a comment at the end";
395451
Assert.EndsWith(Environment.NewLine + "# a comment at the end", script);
396452

397453
// NOTE: This is horribly complicated, but the "script" parameter here is assigned to
@@ -400,15 +456,16 @@ public async Task CanLaunchScriptWithCommentedLastLineAsync()
400456
// ConfigurationDoneHandler in LaunchScriptAsync.
401457
await client.LaunchScript(script);
402458

403-
ConfigurationDoneResponse configDoneResponse = await client.RequestConfigurationDone(new ConfigurationDoneArguments());
404-
Assert.NotNull(configDoneResponse);
459+
_ = await client.RequestConfigurationDone(new ConfigurationDoneArguments());
460+
405461
// We can check that the script was invoked as expected, which is to dot-source a script
406462
// block with the contents surrounded by newlines. While we can't check that the last
407463
// line was a curly brace by itself, we did check that the contents ended with a
408464
// comment, so if this output exists then the bug did not recur.
409-
Assert.Collection(await GetLog(),
410-
(i) => Assert.Equal(". {", i),
411-
(i) => Assert.Equal("", i));
465+
Assert.Equal(". {", await ReadScriptLogLineAsync());
466+
467+
// Verifies that the script did run and the body was evaluated
468+
Assert.Equal("2", await ReadScriptLogLineAsync());
412469
}
413470

414471
[SkippableFact]
@@ -441,14 +498,14 @@ public async Task CanRunPesterTestFile()
441498
{ throw 'error' } | Should -Throw
442499
}
443500
It 'D' {
444-
" + GenerateScriptFromLoggingStatements("pester") + @"
501+
" + GenerateLoggingScript("pester") + @"
445502
}
446503
}
447504
}", isPester: true);
448505

449506
await client.LaunchScript($"Invoke-Pester -Script '{pesterTest}'");
450507
await client.RequestConfigurationDone(new ConfigurationDoneArguments());
451-
Assert.Collection(await GetLog(), (i) => Assert.Equal("pester", i));
508+
Assert.Equal("pester", await ReadScriptLogLineAsync());
452509
}
453510
}
454511
}

0 commit comments

Comments
 (0)