@@ -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