55
66using System ;
77using System . IO ;
8+ using System . Linq ;
89using System . Reflection ;
10+ using System . Runtime . InteropServices ;
11+ using System . Text ;
12+ using System . Threading ;
913using System . Threading . Tasks ;
10- using Microsoft . PowerShell . EditorServices . Handlers ;
11- using Xunit ;
14+ using Microsoft . Extensions . Logging ;
1215using OmniSharp . Extensions . DebugAdapter . Client ;
16+ using OmniSharp . Extensions . DebugAdapter . Protocol . Models ;
1317using OmniSharp . Extensions . DebugAdapter . Protocol . Requests ;
14- using System . Threading ;
18+ using Xunit ;
19+ using Xunit . Abstractions ;
1520
1621namespace PowerShellEditorServices . Test . E2E
1722{
18- public class DebugAdapterProtocolMessageTests : IClassFixture < DAPTestsFixture >
23+ public class DebugAdapterProtocolMessageTests : IAsyncLifetime
1924 {
25+ private const string TestOutputFileName = "__dapTestOutputFile.txt" ;
26+ private readonly static bool s_isWindows = RuntimeInformation . IsOSPlatform ( OSPlatform . Windows ) ;
2027 private readonly static string s_binDir =
2128 Path . GetDirectoryName ( Assembly . GetExecutingAssembly ( ) . Location ) ;
29+ private readonly static string s_testOutputPath = Path . Combine ( s_binDir , TestOutputFileName ) ;
2230
23- private readonly DebugAdapterClient PsesDebugAdapterClient ;
24- private readonly DAPTestsFixture _dapTestsFixture ;
31+ private readonly ITestOutputHelper _output ;
32+ private DebugAdapterClient PsesDebugAdapterClient ;
33+ private PsesStdioProcess _psesProcess ;
2534
26- public DebugAdapterProtocolMessageTests ( DAPTestsFixture data )
35+ public TaskCompletionSource < object > Started { get ; } = new TaskCompletionSource < object > ( ) ;
36+
37+ public DebugAdapterProtocolMessageTests ( ITestOutputHelper output )
2738 {
28- _dapTestsFixture = data ;
29- PsesDebugAdapterClient = data . PsesDebugAdapterClient ;
39+ _output = output ;
40+ }
41+
42+ public async Task InitializeAsync ( )
43+ {
44+ var factory = new LoggerFactory ( ) ;
45+ _psesProcess = new PsesStdioProcess ( factory , true ) ;
46+ await _psesProcess . Start ( ) ;
47+
48+ var initialized = new TaskCompletionSource < bool > ( ) ;
49+ PsesDebugAdapterClient = DebugAdapterClient . Create ( options =>
50+ {
51+ options
52+ . WithInput ( _psesProcess . OutputStream )
53+ . WithOutput ( _psesProcess . InputStream )
54+ // The OnStarted delegate gets run when we receive the _Initialized_ event from the server:
55+ // https://microsoft.github.io/debug-adapter-protocol/specification#Events_Initialized
56+ . OnStarted ( ( client , token ) => {
57+ Started . SetResult ( true ) ;
58+ return Task . CompletedTask ;
59+ } )
60+ // The OnInitialized delegate gets run when we first receive the _Initialize_ response:
61+ // https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Initialize
62+ . OnInitialized ( ( client , request , response , token ) => {
63+ initialized . SetResult ( true ) ;
64+ return Task . CompletedTask ;
65+ } ) ;
66+ } ) ;
67+
68+ // PSES follows the following flow:
69+ // Receive a Initialize request
70+ // Run Initialize handler and send response back
71+ // Receive a Launch/Attach request
72+ // Run Launch/Attach handler and send response back
73+ // PSES sends the initialized event at the end of the Launch/Attach handler
74+
75+ // The way that the Omnisharp client works is that this Initialize method doesn't return until
76+ // after OnStarted is run... which only happens when Initialized is received from the server.
77+ // so if we would await this task, it would deadlock.
78+ // To get around this, we run the Initialize() without await but use a `TaskCompletionSource<bool>`
79+ // that gets completed when we receive the response to Initialize
80+ // This tells us that we are ready to send messages to PSES... but are not stuck waiting for
81+ // Initialized.
82+ PsesDebugAdapterClient . Initialize ( CancellationToken . None ) . ConfigureAwait ( false ) ;
83+ await initialized . Task . ConfigureAwait ( false ) ;
84+ }
85+
86+ public async Task DisposeAsync ( )
87+ {
88+ try
89+ {
90+ await PsesDebugAdapterClient . RequestDisconnect ( new DisconnectArguments
91+ {
92+ Restart = false ,
93+ TerminateDebuggee = true
94+ } ) . ConfigureAwait ( false ) ;
95+ await _psesProcess . Stop ( ) . ConfigureAwait ( false ) ;
96+ PsesDebugAdapterClient ? . Dispose ( ) ;
97+ }
98+ catch ( ObjectDisposedException )
99+ {
100+ // Language client has a disposal bug in it
101+ }
30102 }
31103
32104 private string NewTestFile ( string script , bool isPester = false )
@@ -38,6 +110,32 @@ private string NewTestFile(string script, bool isPester = false)
38110 return filePath ;
39111 }
40112
113+ private string GenerateScriptFromLoggingStatements ( params string [ ] logStatements )
114+ {
115+ if ( logStatements . Length == 0 )
116+ {
117+ throw new ArgumentNullException ( "Expected at least one argument." ) ;
118+ }
119+
120+ // Have script create/overwrite file first with `>`.
121+ StringBuilder builder = new StringBuilder ( ) . Append ( '\' ' ) . Append ( logStatements [ 0 ] ) . Append ( "' > '" ) . Append ( s_testOutputPath ) . AppendLine ( "'" ) ;
122+ for ( int i = 1 ; i < logStatements . Length ; i ++ )
123+ {
124+ // Then append to that script with `>>`.
125+ builder . Append ( '\' ' ) . Append ( logStatements [ i ] ) . Append ( "' >> '" ) . Append ( s_testOutputPath ) . AppendLine ( "'" ) ;
126+ }
127+
128+ _output . WriteLine ( "Script is:" ) ;
129+ _output . WriteLine ( builder . ToString ( ) ) ;
130+ return builder . ToString ( ) ;
131+ }
132+
133+ private string [ ] GetLog ( )
134+ {
135+ return File . ReadLines ( s_testOutputPath ) . ToArray ( ) ;
136+ }
137+
138+ [ Trait ( "Category" , "DAP" ) ]
41139 [ Fact ]
42140 public void CanInitializeWithCorrectServerSettings ( )
43141 {
@@ -49,34 +147,88 @@ public void CanInitializeWithCorrectServerSettings()
49147 Assert . True ( PsesDebugAdapterClient . ServerSettings . SupportsSetVariable ) ;
50148 }
51149
150+ [ Trait ( "Category" , "DAP" ) ]
52151 [ Fact ]
53152 public async Task CanLaunchScriptWithNoBreakpointsAsync ( )
54153 {
55- string filePath = NewTestFile ( "'works' > \" $PSScriptRoot/testFile.txt\" " ) ;
56- LaunchResponse launchResponse = await PsesDebugAdapterClient . RequestLaunch ( new PsesLaunchRequestArguments
154+ string filePath = NewTestFile ( GenerateScriptFromLoggingStatements ( "works" ) ) ;
155+
156+ await PsesDebugAdapterClient . LaunchScript ( filePath , Started ) . ConfigureAwait ( false ) ;
157+
158+ ConfigurationDoneResponse configDoneResponse = await PsesDebugAdapterClient . RequestConfigurationDone ( new ConfigurationDoneArguments ( ) ) . ConfigureAwait ( false ) ;
159+ Assert . NotNull ( configDoneResponse ) ;
160+
161+ // At this point the script should be running so lets give it time
162+ await Task . Delay ( 2000 ) . ConfigureAwait ( false ) ;
163+
164+ string [ ] log = GetLog ( ) ;
165+ Assert . Equal ( "works" , log [ 0 ] ) ;
166+ }
167+
168+ [ Trait ( "Category" , "DAP" ) ]
169+ [ SkippableFact ]
170+ public async Task CanSetBreakpointsAsync ( )
171+ {
172+ Skip . If (
173+ PsesStdioProcess . RunningInConstainedLanguageMode ,
174+ "You can't set breakpoints in ConstrainedLanguage mode." ) ;
175+
176+ string filePath = NewTestFile ( GenerateScriptFromLoggingStatements (
177+ "before breakpoint" ,
178+ "at breakpoint" ,
179+ "after breakpoint"
180+ ) ) ;
181+
182+ await PsesDebugAdapterClient . LaunchScript ( filePath , Started ) . ConfigureAwait ( false ) ;
183+
184+ // {"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}
185+ SetBreakpointsResponse setBreakpointsResponse = await PsesDebugAdapterClient . RequestSetBreakpoints ( new SetBreakpointsArguments
57186 {
58- NoDebug = false ,
59- Script = filePath ,
60- Cwd = "" ,
61- CreateTemporaryIntegratedConsole = false ,
187+ Source = new Source
188+ {
189+ Name = Path . GetFileName ( filePath ) ,
190+ Path = filePath
191+ } ,
192+ Lines = new long [ ] { 2 } ,
193+ Breakpoints = new SourceBreakpoint [ ]
194+ {
195+ new SourceBreakpoint
196+ {
197+ Line = 2 ,
198+ }
199+ } ,
200+ SourceModified = false ,
62201 } ) . ConfigureAwait ( false ) ;
63202
64- Assert . NotNull ( launchResponse ) ;
65-
66- // This will check to see if we received the Initialized event from the server.
67- await Task . Run (
68- async ( ) => await _dapTestsFixture . Started . Task . ConfigureAwait ( false ) ,
69- new CancellationTokenSource ( 2000 ) . Token ) . ConfigureAwait ( false ) ;
203+ var breakpoint = setBreakpointsResponse . Breakpoints . First ( ) ;
204+ Assert . True ( breakpoint . Verified ) ;
205+ Assert . Equal ( filePath , breakpoint . Source . Path , ignoreCase : s_isWindows ) ;
206+ Assert . Equal ( 2 , breakpoint . Line ) ;
70207
71208 ConfigurationDoneResponse configDoneResponse = await PsesDebugAdapterClient . RequestConfigurationDone ( new ConfigurationDoneArguments ( ) ) . ConfigureAwait ( false ) ;
72209 Assert . NotNull ( configDoneResponse ) ;
73210
74211 // At this point the script should be running so lets give it time
75212 await Task . Delay ( 2000 ) . ConfigureAwait ( false ) ;
76213
77- string testFile = Path . Join ( Path . GetDirectoryName ( filePath ) , "testFile.txt" ) ;
78- string contents = await File . ReadAllTextAsync ( testFile ) . ConfigureAwait ( false ) ;
79- Assert . Equal ( $ "works{ Environment . NewLine } ", contents ) ;
214+ string [ ] log = GetLog ( ) ;
215+ Assert . Single ( log , ( i ) => i == "before breakpoint" ) ;
216+
217+ ContinueResponse continueResponse = await PsesDebugAdapterClient . RequestContinue ( new ContinueArguments
218+ {
219+ ThreadId = 1 ,
220+ } ) . ConfigureAwait ( true ) ;
221+
222+ Assert . NotNull ( continueResponse ) ;
223+
224+ // At this point the script should be running so lets give it time
225+ await Task . Delay ( 2000 ) . ConfigureAwait ( false ) ;
226+
227+ log = GetLog ( ) ;
228+ Assert . Collection ( log ,
229+ ( i ) => Assert . Equal ( "before breakpoint" , i ) ,
230+ ( i ) => Assert . Equal ( "at breakpoint" , i ) ,
231+ ( i ) => Assert . Equal ( "after breakpoint" , i ) ) ;
80232 }
81233 }
82234}
0 commit comments