@@ -30,21 +30,38 @@ public class DebugAdapterProtocolMessageTests(ITestOutputHelper output) : IAsync
30
30
private IDebugAdapterClient client ;
31
31
32
32
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 ( ) ) ;
34
38
35
39
private readonly PsesStdioLanguageServerProcessHost psesHost = new ( isDebugAdapter : true ) ;
36
40
37
41
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>
38
45
private Task < IDebugAdapterClient > initializedLanguageClient => initializedLanguageClientTcs . Task ;
39
46
40
47
/// <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
+
43
51
private TaskCompletionSource < StoppedEvent > nextStoppedTcs = new ( ) ;
52
+ /// <summary>
53
+ /// This task is useful for waiting until a breakpoint is hit in a test.
54
+ /// </summary>
44
55
private Task < StoppedEvent > nextStopped => nextStoppedTcs . Task ;
45
56
46
57
public async Task InitializeAsync ( )
47
58
{
59
+ // Cleanup testScriptLogPath if it exists due to an interrupted previous run
60
+ if ( File . Exists ( testScriptLogPath ) )
61
+ {
62
+ File . Delete ( testScriptLogPath ) ;
63
+ }
64
+
48
65
( StreamReader stdout , StreamWriter stdin ) = await psesHost . Start ( ) ;
49
66
50
67
// Splice the streams together and enable debug logging of all messages sent and received
@@ -106,6 +123,12 @@ await client.RequestDisconnect(new DisconnectArguments
106
123
} ) ;
107
124
client ? . Dispose ( ) ;
108
125
psesHost . Stop ( ) ;
126
+
127
+ scriptLogReader ? . Dispose ( ) ; //Also disposes the underlying filestream
128
+ if ( File . Exists ( testScriptLogPath ) )
129
+ {
130
+ File . Delete ( testScriptLogPath ) ;
131
+ }
109
132
}
110
133
111
134
private static string NewTestFile ( string script , bool isPester = false )
@@ -117,17 +140,24 @@ private static string NewTestFile(string script, bool isPester = false)
117
140
return filePath ;
118
141
}
119
142
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 )
121
151
{
122
152
if ( logStatements . Length == 0 )
123
153
{
124
154
throw new ArgumentNullException ( nameof ( logStatements ) , "Expected at least one argument." ) ;
125
155
}
126
156
127
157
// Clean up side effects from other test runs.
128
- if ( File . Exists ( s_testTempPath ) )
158
+ if ( File . Exists ( testScriptLogPath ) )
129
159
{
130
- File . Delete ( s_testTempPath ) ;
160
+ File . Delete ( testScriptLogPath ) ;
131
161
}
132
162
133
163
// Have script create file first with `>` (but don't rely on overwriting).
@@ -136,7 +166,7 @@ private string GenerateScriptFromLoggingStatements(params string[] logStatements
136
166
. Append ( "Write-Output \" " )
137
167
. Append ( logStatements [ 0 ] )
138
168
. Append ( "\" > '" )
139
- . Append ( s_testTempPath )
169
+ . Append ( testScriptLogPath )
140
170
. AppendLine ( "'" ) ;
141
171
142
172
for ( int i = 1 ; i < logStatements . Length ; i ++ )
@@ -146,7 +176,7 @@ private string GenerateScriptFromLoggingStatements(params string[] logStatements
146
176
. Append ( "Write-Output \" " )
147
177
. Append ( logStatements [ i ] )
148
178
. Append ( "\" >> '" )
149
- . Append ( s_testTempPath )
179
+ . Append ( testScriptLogPath )
150
180
. AppendLine ( "'" ) ;
151
181
}
152
182
@@ -155,15 +185,37 @@ private string GenerateScriptFromLoggingStatements(params string[] logStatements
155
185
return builder . ToString ( ) ;
156
186
}
157
187
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 ( )
159
192
{
160
- for ( int i = 0 ; ! File . Exists ( s_testTempPath ) && i < 60 ; i ++ )
193
+ while ( scriptLogReader is null )
161
194
{
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
163
217
}
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 ;
167
219
}
168
220
169
221
[ Fact ]
@@ -181,36 +233,35 @@ public void CanInitializeWithCorrectServerSettings()
181
233
[ Fact ]
182
234
public async Task UsesDotSourceOperatorAndQuotesAsync ( )
183
235
{
184
- string filePath = NewTestFile ( GenerateScriptFromLoggingStatements ( "$($MyInvocation.Line)" ) ) ;
236
+ string filePath = NewTestFile ( GenerateLoggingScript ( "$($MyInvocation.Line)" ) ) ;
185
237
await client . LaunchScript ( filePath ) ;
186
238
ConfigurationDoneResponse configDoneResponse = await client . RequestConfigurationDone ( new ConfigurationDoneArguments ( ) ) ;
187
239
Assert . NotNull ( configDoneResponse ) ;
188
- Assert . Collection ( await GetLog ( ) ,
189
- ( i ) => Assert . StartsWith ( ". '" , i ) ) ;
240
+ string actual = await ReadScriptLogLineAsync ( ) ;
241
+ Assert . StartsWith ( ". '" , actual ) ;
190
242
}
191
243
192
244
[ Fact ]
193
245
public async Task UsesCallOperatorWithSettingAsync ( )
194
246
{
195
- string filePath = NewTestFile ( GenerateScriptFromLoggingStatements ( "$($MyInvocation.Line)" ) ) ;
247
+ string filePath = NewTestFile ( GenerateLoggingScript ( "$($MyInvocation.Line)" ) ) ;
196
248
await client . LaunchScript ( filePath , executeMode : "Call" ) ;
197
249
ConfigurationDoneResponse configDoneResponse = await client . RequestConfigurationDone ( new ConfigurationDoneArguments ( ) ) ;
198
250
Assert . NotNull ( configDoneResponse ) ;
199
- Assert . Collection ( await GetLog ( ) ,
200
- ( i ) => Assert . StartsWith ( "& '" , i ) ) ;
251
+ string actual = await ReadScriptLogLineAsync ( ) ;
252
+ Assert . StartsWith ( "& '" , actual ) ;
201
253
}
202
254
203
255
[ Fact ]
204
256
public async Task CanLaunchScriptWithNoBreakpointsAsync ( )
205
257
{
206
- string filePath = NewTestFile ( GenerateScriptFromLoggingStatements ( "works" ) ) ;
258
+ string filePath = NewTestFile ( GenerateLoggingScript ( "works" ) ) ;
207
259
208
260
await client . LaunchScript ( filePath ) ;
209
261
210
262
ConfigurationDoneResponse configDoneResponse = await client . RequestConfigurationDone ( new ConfigurationDoneArguments ( ) ) ;
211
263
Assert . NotNull ( configDoneResponse ) ;
212
- Assert . Collection ( await GetLog ( ) ,
213
- ( i ) => Assert . Equal ( "works" , i ) ) ;
264
+ Assert . Equal ( "works" , await ReadScriptLogLineAsync ( ) ) ;
214
265
}
215
266
216
267
[ SkippableFact ]
@@ -219,7 +270,7 @@ public async Task CanSetBreakpointsAsync()
219
270
Skip . If ( PsesStdioLanguageServerProcessHost . RunningInConstrainedLanguageMode ,
220
271
"Breakpoints can't be set in Constrained Language Mode." ) ;
221
272
222
- string filePath = NewTestFile ( GenerateScriptFromLoggingStatements (
273
+ string filePath = NewTestFile ( GenerateLoggingScript (
223
274
"before breakpoint" ,
224
275
"at breakpoint" ,
225
276
"after breakpoint"
@@ -243,32 +294,36 @@ public async Task CanSetBreakpointsAsync()
243
294
ConfigurationDoneResponse configDoneResponse = await client . RequestConfigurationDone ( new ConfigurationDoneArguments ( ) ) ;
244
295
Assert . NotNull ( configDoneResponse ) ;
245
296
297
+ // Wait until we hit the breakpoint
246
298
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 ( ) ) ;
247
303
248
304
// Assert that the stopped breakpoint is the one we set
249
305
StackTraceResponse stackTraceResponse = await client . RequestStackTrace ( new StackTraceArguments { ThreadId = 1 } ) ;
250
306
DapStackFrame stoppedTopFrame = stackTraceResponse . StackFrames . First ( ) ;
251
307
Assert . Equal ( 2 , stoppedTopFrame . Line ) ;
252
308
309
+ _ = await client . RequestContinue ( new ContinueArguments { ThreadId = 1 } ) ;
253
310
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 ( ) ) ;
261
313
}
262
314
263
315
[ SkippableFact ]
264
316
public async Task FailsIfStacktraceRequestedWhenNotPaused ( )
265
317
{
266
318
Skip . If ( PsesStdioLanguageServerProcessHost . RunningInConstrainedLanguageMode ,
267
319
"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"
270
325
) ) ;
271
- // Set a breakpoint
326
+
272
327
await client . SetBreakpoints (
273
328
new SetBreakpointsArguments
274
329
{
@@ -282,7 +337,7 @@ await client.SetBreakpoints(
282
337
await client . RequestConfigurationDone ( new ConfigurationDoneArguments ( ) ) ;
283
338
await client . LaunchScript ( filePath ) ;
284
339
285
- // Get the stacktrace for the breakpoint
340
+ // Try to get the stacktrace. If we are not at a breakpoint, this should fail.
286
341
await Assert . ThrowsAsync < JsonRpcException > ( ( ) => client . RequestStackTrace (
287
342
new StackTraceArguments { }
288
343
) ) ;
@@ -293,17 +348,14 @@ public async Task SendsInitialLabelBreakpointForPerformanceReasons()
293
348
{
294
349
Skip . If ( PsesStdioLanguageServerProcessHost . RunningInConstrainedLanguageMode ,
295
350
"Breakpoints can't be set in Constrained Language Mode." ) ;
296
- string filePath = NewTestFile ( GenerateScriptFromLoggingStatements (
351
+ string filePath = NewTestFile ( GenerateLoggingScript (
297
352
"before breakpoint" ,
298
- "at breakpoint" ,
299
- "after breakpoint"
353
+ "label breakpoint"
300
354
) ) ;
301
355
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 .
303
357
await client . LaunchScript ( filePath ) ;
304
358
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}
307
359
SetBreakpointsResponse setBreakpointsResponse = await client . SetBreakpoints ( new SetBreakpointsArguments
308
360
{
309
361
Source = new Source { Name = Path . GetFileName ( filePath ) , Path = filePath } ,
@@ -318,16 +370,20 @@ public async Task SendsInitialLabelBreakpointForPerformanceReasons()
318
370
319
371
_ = client . RequestConfigurationDone ( new ConfigurationDoneArguments ( ) ) ;
320
372
321
- // Resume when the next breakpoint hits
373
+ // Wait for the breakpoint to be hit
322
374
StoppedEvent stoppedEvent = await nextStopped ;
323
-
324
375
Assert . Equal ( "breakpoint" , stoppedEvent . Reason ) ;
325
376
377
+ // The code before the breakpoint should have already run
378
+ Assert . Equal ( "before breakpoint" , await ReadScriptLogLineAsync ( ) ) ;
379
+
326
380
// Get the stacktrace for the breakpoint
327
381
StackTraceResponse stackTraceResponse = await client . RequestStackTrace (
328
382
new StackTraceArguments { ThreadId = 1 }
329
383
) ;
330
384
DapStackFrame firstFrame = stackTraceResponse . StackFrames . First ( ) ;
385
+
386
+ // Our synthetic label breakpoint should be present
331
387
Assert . Equal (
332
388
StackFramePresentationHint . Label ,
333
389
firstFrame . PresentationHint
@@ -391,7 +447,7 @@ public async Task CanStepPastSystemWindowsForms()
391
447
[ Fact ]
392
448
public async Task CanLaunchScriptWithCommentedLastLineAsync ( )
393
449
{
394
- string script = GenerateScriptFromLoggingStatements ( "$($MyInvocation.Line)" ) + "# a comment at the end" ;
450
+ string script = GenerateLoggingScript ( "$($MyInvocation.Line)" , "$(1+1 )") + "# a comment at the end" ;
395
451
Assert . EndsWith ( Environment . NewLine + "# a comment at the end" , script ) ;
396
452
397
453
// NOTE: This is horribly complicated, but the "script" parameter here is assigned to
@@ -400,15 +456,16 @@ public async Task CanLaunchScriptWithCommentedLastLineAsync()
400
456
// ConfigurationDoneHandler in LaunchScriptAsync.
401
457
await client . LaunchScript ( script ) ;
402
458
403
- ConfigurationDoneResponse configDoneResponse = await client . RequestConfigurationDone ( new ConfigurationDoneArguments ( ) ) ;
404
- Assert . NotNull ( configDoneResponse ) ;
459
+ _ = await client . RequestConfigurationDone ( new ConfigurationDoneArguments ( ) ) ;
460
+
405
461
// We can check that the script was invoked as expected, which is to dot-source a script
406
462
// block with the contents surrounded by newlines. While we can't check that the last
407
463
// line was a curly brace by itself, we did check that the contents ended with a
408
464
// 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 ( ) ) ;
412
469
}
413
470
414
471
[ SkippableFact ]
@@ -441,14 +498,14 @@ public async Task CanRunPesterTestFile()
441
498
{ throw 'error' } | Should -Throw
442
499
}
443
500
It 'D' {
444
- " + GenerateScriptFromLoggingStatements ( "pester" ) + @"
501
+ " + GenerateLoggingScript ( "pester" ) + @"
445
502
}
446
503
}
447
504
}" , isPester : true ) ;
448
505
449
506
await client . LaunchScript ( $ "Invoke-Pester -Script '{ pesterTest } '") ;
450
507
await client . RequestConfigurationDone ( new ConfigurationDoneArguments ( ) ) ;
451
- Assert . Collection ( await GetLog ( ) , ( i ) => Assert . Equal ( "pester" , i ) ) ;
508
+ Assert . Equal ( "pester" , await ReadScriptLogLineAsync ( ) ) ;
452
509
}
453
510
}
454
511
}
0 commit comments