@@ -30,163 +30,180 @@ export enum Mode {
30
30
31
31
// The StreamingRunner class is responsible for executing the test process or launching the debugger while handling the
32
32
// streaming events to update the test explorer status
33
- export class StreamingRunner {
34
- private readonly promises : Promise < void > [ ] = [ ] ;
35
- private readonly disposables : vscode . Disposable [ ] = [ ] ;
36
- private readonly run : vscode . TestRun ;
33
+ export class StreamingRunner implements vscode . Disposable {
34
+ private promises : Promise < void > [ ] = [ ] ;
35
+ private disposables : vscode . Disposable [ ] = [ ] ;
37
36
private readonly findTestItem : (
38
37
id : string ,
39
38
uri : vscode . Uri ,
40
39
) => Promise < vscode . TestItem | undefined > ;
41
40
41
+ private readonly tcpServer : net . Server ;
42
+ private tcpPort : string | undefined ;
43
+ private connection : rpc . MessageConnection | undefined ;
44
+ private executionPromise :
45
+ | { resolve : ( ) => void ; reject : ( error : Error ) => void }
46
+ | undefined ;
47
+
48
+ private run : vscode . TestRun | undefined ;
49
+
42
50
constructor (
43
- run : vscode . TestRun ,
44
51
findTestItem : (
45
52
id : string ,
46
53
uri : vscode . Uri ,
47
54
) => Promise < vscode . TestItem | undefined > ,
48
55
) {
49
- this . run = run ;
50
56
this . findTestItem = findTestItem ;
57
+ this . tcpServer = this . startServer ( ) ;
51
58
}
52
59
53
60
async execute (
61
+ currentRun : vscode . TestRun ,
54
62
command : string ,
55
63
env : NodeJS . ProcessEnv ,
56
64
workspace : Workspace ,
57
65
mode : Mode ,
58
66
linkedCancellationSource : LinkedCancellationSource ,
59
67
) {
68
+ this . run = currentRun ;
69
+
60
70
await new Promise < void > ( ( resolve , reject ) => {
61
- const server = net . createServer ( ) ;
62
- server . on ( "error" , reject ) ;
63
- server . unref ( ) ;
64
-
65
- server . listen ( 0 , "localhost" , async ( ) => {
66
- const address = server . address ( ) ;
67
- const serverPort =
68
- typeof address === "string" ? address : address ?. port . toString ( ) ;
69
-
70
- if ( ! serverPort ) {
71
- reject (
72
- new Error (
73
- "Failed to set up TCP server to communicate with test process" ,
74
- ) ,
75
- ) ;
76
- return ;
77
- }
78
-
79
- const abortController = new AbortController ( ) ;
80
-
81
- server . on ( "connection" , ( socket ) => {
82
- const connection = rpc . createMessageConnection (
83
- new rpc . StreamMessageReader ( socket ) ,
84
- new rpc . StreamMessageWriter ( socket ) ,
85
- ) ;
86
- const finalize = ( ) => {
87
- Promise . all ( this . promises )
88
- . then ( ( ) => {
89
- this . disposables . forEach ( ( disposable ) => disposable . dispose ( ) ) ;
90
- connection . end ( ) ;
91
- connection . dispose ( ) ;
92
- server . close ( ) ;
93
- resolve ( ) ;
94
- } )
95
- . catch ( reject ) ;
96
- } ;
97
-
98
- // We resolve the promise and perform cleanup on two occasions: if the test run finished normally, then we
99
- // should receive the finish event. The other case is when the run is cancelled and the abort controller gets
100
- // triggered, in which case we will not receive the finish event
101
- linkedCancellationSource . onCancellationRequested ( ( ) => {
102
- this . run . appendOutput ( "\r\nTest run cancelled." ) ;
103
- abortController . abort ( ) ;
104
- finalize ( ) ;
105
- } ) ;
106
-
107
- this . disposables . push (
108
- connection . onNotification ( NOTIFICATION_TYPES . finish , finalize ) ,
109
- ) ;
110
-
111
- this . registerStreamingEvents ( connection ) ;
112
-
113
- // Start listening for events
114
- connection . listen ( ) ;
115
- } ) ;
116
-
117
- if ( mode === Mode . Run ) {
118
- this . spawnTestProcess (
119
- command ,
120
- env ,
121
- workspace . workspaceFolder . uri . fsPath ,
122
- serverPort ,
123
- abortController ,
124
- ) ;
125
- } else {
126
- await this . launchDebugger ( command , env , workspace , serverPort ) ;
127
- }
71
+ this . executionPromise = { resolve, reject } ;
72
+ const abortController = new AbortController ( ) ;
73
+
74
+ linkedCancellationSource . onCancellationRequested ( async ( ) => {
75
+ this . run ! . appendOutput ( "\r\nTest run cancelled." ) ;
76
+ abortController . abort ( ) ;
77
+ await this . finalize ( ) ;
128
78
} ) ;
79
+
80
+ if ( mode === Mode . Run ) {
81
+ this . spawnTestProcess (
82
+ command ,
83
+ env ,
84
+ workspace . workspaceFolder . uri . fsPath ,
85
+ abortController ,
86
+ ) ;
87
+ } else {
88
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
89
+ this . launchDebugger ( command , env , workspace ) ;
90
+ }
129
91
} ) ;
130
92
}
131
93
94
+ dispose ( ) {
95
+ this . tcpServer . close ( ) ;
96
+ this . connection ?. dispose ( ) ;
97
+ }
98
+
132
99
// Launches the debugger with streaming updates
133
100
private async launchDebugger (
134
101
command : string ,
135
102
env : NodeJS . ProcessEnv ,
136
103
workspace : Workspace ,
137
- serverPort : string ,
138
104
) {
139
- await vscode . debug
140
- . startDebugging (
141
- workspace . workspaceFolder ,
142
- {
143
- type : "ruby_lsp" ,
144
- name : "Debug" ,
145
- request : "launch" ,
146
- program : command ,
147
- env : {
148
- ...env ,
149
- DISABLE_SPRING : "1" ,
150
- RUBY_LSP_REPORTER_PORT : serverPort ,
151
- } ,
105
+ const successFullyStarted = await vscode . debug . startDebugging (
106
+ workspace . workspaceFolder ,
107
+ {
108
+ type : "ruby_lsp" ,
109
+ name : "Debug" ,
110
+ request : "launch" ,
111
+ program : command ,
112
+ env : {
113
+ ...env ,
114
+ DISABLE_SPRING : "1" ,
115
+ RUBY_LSP_REPORTER_PORT : this . tcpPort ,
152
116
} ,
153
- { testRun : this . run } ,
154
- )
155
- . then ( ( successFullyStarted ) => {
156
- if ( ! successFullyStarted ) {
157
- throw new Error ( "Failed to start debugging session" ) ;
158
- }
159
- } ) ;
117
+ } ,
118
+ { testRun : this . run } ,
119
+ ) ;
120
+
121
+ if ( ! successFullyStarted ) {
122
+ this . executionPromise ! . reject (
123
+ new Error ( "Failed to start debugging session" ) ,
124
+ ) ;
125
+ }
160
126
}
161
127
162
128
// Spawns the test process and redirects any stdout or stderr output to the test run output
163
129
private spawnTestProcess (
164
130
command : string ,
165
131
env : NodeJS . ProcessEnv ,
166
132
cwd : string ,
167
- serverPort : string ,
168
133
abortController : AbortController ,
169
134
) {
170
135
const testProcess = spawn ( command , {
171
- env : { ...env , RUBY_LSP_REPORTER_PORT : serverPort } ,
136
+ env : { ...env , RUBY_LSP_REPORTER_PORT : this . tcpPort } ,
172
137
stdio : [ "pipe" , "pipe" , "pipe" ] ,
173
138
shell : true ,
174
139
signal : abortController . signal ,
175
140
cwd,
176
141
} ) ;
177
142
178
143
testProcess . stdout . on ( "data" , ( data ) => {
179
- this . run . appendOutput ( data . toString ( ) . replace ( / \n / g, "\r\n" ) ) ;
144
+ this . run ! . appendOutput ( data . toString ( ) . replace ( / \n / g, "\r\n" ) ) ;
180
145
} ) ;
181
146
182
147
testProcess . stderr . on ( "data" , ( data ) => {
183
- this . run . appendOutput ( data . toString ( ) . replace ( / \n / g, "\r\n" ) ) ;
148
+ this . run ! . appendOutput ( data . toString ( ) . replace ( / \n / g, "\r\n" ) ) ;
149
+ } ) ;
150
+ }
151
+
152
+ private startServer ( ) {
153
+ const server = net . createServer ( ) ;
154
+ server . on ( "error" , ( error ) => {
155
+ throw error ;
156
+ } ) ;
157
+ server . unref ( ) ;
158
+
159
+ server . listen ( 0 , "localhost" , ( ) => {
160
+ const address = server . address ( ) ;
161
+
162
+ if ( ! address ) {
163
+ throw new Error ( "Failed setup TCP server for streaming updates" ) ;
164
+ }
165
+ this . tcpPort =
166
+ typeof address === "string" ? address : address . port . toString ( ) ;
167
+
168
+ // On any new connection to the TCP server, attach the JSON RPC reader and the events we defined
169
+ server . on ( "connection" , ( socket ) => {
170
+ this . connection = rpc . createMessageConnection (
171
+ new rpc . StreamMessageReader ( socket ) ,
172
+ new rpc . StreamMessageWriter ( socket ) ,
173
+ ) ;
174
+
175
+ // Register and start listening for events
176
+ this . registerStreamingEvents ( ) ;
177
+ this . connection . listen ( ) ;
178
+ } ) ;
184
179
} ) ;
180
+
181
+ return server ;
182
+ }
183
+
184
+ private async finalize ( ) {
185
+ await Promise . all ( this . promises ) ;
186
+
187
+ this . disposables . forEach ( ( disposable ) => disposable . dispose ( ) ) ;
188
+
189
+ this . promises = [ ] ;
190
+ this . disposables = [ ] ;
191
+
192
+ if ( this . connection ) {
193
+ this . connection . end ( ) ;
194
+ this . connection . dispose ( ) ;
195
+ }
196
+
197
+ this . executionPromise ! . resolve ( ) ;
185
198
}
186
199
187
200
// Registers all streaming events that we will receive from the server except for the finish event, which is
188
201
// registered to resolve the execute promise
189
- private registerStreamingEvents ( connection : rpc . MessageConnection ) {
202
+ private registerStreamingEvents ( ) {
203
+ if ( ! this . connection ) {
204
+ return ;
205
+ }
206
+
190
207
const startTimestamps = new Map < string , number > ( ) ;
191
208
const withDuration = (
192
209
id : string ,
@@ -199,12 +216,19 @@ export class StreamingRunner {
199
216
200
217
// Handle the JSON events being emitted by the tests
201
218
this . disposables . push (
202
- connection . onNotification ( NOTIFICATION_TYPES . start , ( params ) => {
219
+ this . connection . onNotification (
220
+ NOTIFICATION_TYPES . finish ,
221
+ this . finalize . bind ( this ) ,
222
+ ) ,
223
+ ) ;
224
+
225
+ this . disposables . push (
226
+ this . connection . onNotification ( NOTIFICATION_TYPES . start , ( params ) => {
203
227
this . promises . push (
204
228
this . findTestItem ( params . id , vscode . Uri . parse ( params . uri ) ) . then (
205
229
( test ) => {
206
230
if ( test ) {
207
- this . run . started ( test ) ;
231
+ this . run ! . started ( test ) ;
208
232
startTimestamps . set ( test . id , Date . now ( ) ) ;
209
233
}
210
234
} ,
@@ -214,13 +238,13 @@ export class StreamingRunner {
214
238
) ;
215
239
216
240
this . disposables . push (
217
- connection . onNotification ( NOTIFICATION_TYPES . pass , ( params ) => {
241
+ this . connection . onNotification ( NOTIFICATION_TYPES . pass , ( params ) => {
218
242
this . promises . push (
219
243
this . findTestItem ( params . id , vscode . Uri . parse ( params . uri ) ) . then (
220
244
( test ) => {
221
245
if ( test ) {
222
246
withDuration ( test . id , ( duration ) =>
223
- this . run . passed ( test , duration ) ,
247
+ this . run ! . passed ( test , duration ) ,
224
248
) ;
225
249
}
226
250
} ,
@@ -230,13 +254,13 @@ export class StreamingRunner {
230
254
) ;
231
255
232
256
this . disposables . push (
233
- connection . onNotification ( NOTIFICATION_TYPES . fail , ( params ) => {
257
+ this . connection . onNotification ( NOTIFICATION_TYPES . fail , ( params ) => {
234
258
this . promises . push (
235
259
this . findTestItem ( params . id , vscode . Uri . parse ( params . uri ) ) . then (
236
260
( test ) => {
237
261
if ( test ) {
238
262
withDuration ( test . id , ( duration ) =>
239
- this . run . failed (
263
+ this . run ! . failed (
240
264
test ,
241
265
new vscode . TestMessage ( params . message ) ,
242
266
duration ,
@@ -250,13 +274,13 @@ export class StreamingRunner {
250
274
) ;
251
275
252
276
this . disposables . push (
253
- connection . onNotification ( NOTIFICATION_TYPES . error , ( params ) => {
277
+ this . connection . onNotification ( NOTIFICATION_TYPES . error , ( params ) => {
254
278
this . promises . push (
255
279
this . findTestItem ( params . id , vscode . Uri . parse ( params . uri ) ) . then (
256
280
( test ) => {
257
281
if ( test ) {
258
282
withDuration ( test . id , ( duration ) =>
259
- this . run . errored (
283
+ this . run ! . errored (
260
284
test ,
261
285
new vscode . TestMessage ( params . message ) ,
262
286
duration ,
@@ -270,12 +294,12 @@ export class StreamingRunner {
270
294
) ;
271
295
272
296
this . disposables . push (
273
- connection . onNotification ( NOTIFICATION_TYPES . skip , ( params ) => {
297
+ this . connection . onNotification ( NOTIFICATION_TYPES . skip , ( params ) => {
274
298
this . promises . push (
275
299
this . findTestItem ( params . id , vscode . Uri . parse ( params . uri ) ) . then (
276
300
( test ) => {
277
301
if ( test ) {
278
- this . run . skipped ( test ) ;
302
+ this . run ! . skipped ( test ) ;
279
303
}
280
304
} ,
281
305
) ,
0 commit comments