11import { spawn } from 'node:child_process' ;
2- import { createServer } from 'node:http' ;
32import path , { dirname } from 'node:path' ;
4- import getPort from 'get-port' ;
53import vscode from 'vscode' ;
6- import type { WebSocket } from 'ws' ;
7- import { WebSocketServer } from 'ws' ;
84import { getConfigValue } from './config' ;
95import { logger } from './logger' ;
10- import type {
11- WorkerEvent ,
12- WorkerEventFinish ,
13- WorkerRunTestData ,
14- } from './types' ;
6+ import type { LogLevel } from './shared/logger' ;
7+ import { expose , wrap } from './shared/rpc' ;
8+ import type { WorkerRunTestData } from './types' ;
9+ import type { Worker } from './worker' ;
1510
1611export class RstestApi {
17- public ws : WebSocket | null = null ;
18- private testPromises : Map <
19- string ,
20- { resolve : ( value : any ) => void ; reject : ( reason ?: any ) => void }
21- > = new Map ( ) ;
12+ public worker : Pick < Worker , 'initRstest' | 'runTest' > | null = null ;
2213 private versionMismatchWarned = false ;
2314
2415 public resolveRstestPath ( ) : { cwd : string ; rstestPath : string } [ ] {
@@ -116,81 +107,69 @@ export class RstestApi {
116107 }
117108
118109 public async runTest ( item : vscode . TestItem ) {
119- if ( this . ws ) {
110+ if ( this . worker ) {
120111 const data : WorkerRunTestData = {
121- type : 'runTest' ,
122112 id : item . id ,
123113 fileFilters : [ item . uri ! . fsPath ] ,
124114 testNamePattern : item . label ,
125115 } ;
126116
127- // Create a promise that will be resolved when we get a response with the matching ID
128- const promise = new Promise < any > ( ( resolve , reject ) => {
129- this . testPromises . set ( item . id , { resolve, reject } ) ;
130-
131- // Set a timeout to prevent hanging indefinitely
132- setTimeout ( ( ) => {
133- const promiseObj = this . testPromises . get ( item . id ) ;
134- if ( promiseObj ) {
135- this . testPromises . delete ( item . id ) ;
136- reject ( new Error ( `Test execution timed out for ${ item . label } ` ) ) ;
137- }
138- } , 10000 ) ; // 10 seconds timeout
139- } ) ;
140-
141- this . ws . send ( JSON . stringify ( data ) ) ;
142- return promise ;
117+ return Promise . race ( [
118+ this . worker . runTest ( data ) ,
119+ new Promise ( ( _ , reject ) =>
120+ setTimeout (
121+ reject ,
122+ 10_000 ,
123+ new Error ( `Test execution timed out for ${ item . label } ` ) ,
124+ ) ,
125+ ) , // 10 seconds timeout
126+ ] ) ;
143127 }
144128 }
145129
146130 public async runFileTests ( fileItem : vscode . TestItem ) {
147- if ( this . ws ) {
131+ if ( this . worker ) {
148132 const fileId = `file_${ fileItem . id } ` ;
149133 const data : WorkerRunTestData = {
150- type : 'runTest' ,
151134 id : fileId ,
152135 fileFilters : [ fileItem . uri ! . fsPath ] ,
153136 testNamePattern : '' , // Empty pattern to run all tests in the file
154137 } ;
155138
156- // Create a promise that will be resolved when we get a response with the matching ID
157- const promise = new Promise < WorkerEventFinish > ( ( resolve , reject ) => {
158- this . testPromises . set ( fileId , { resolve, reject } ) ;
159-
160- // Set a timeout to prevent hanging indefinitely
161- setTimeout ( ( ) => {
162- const promiseObj = this . testPromises . get ( fileId ) ;
163- if ( promiseObj ) {
164- this . testPromises . delete ( fileId ) ;
165- reject (
166- new Error (
167- `File test execution timed out for ${ fileItem . uri ! . fsPath } ` ,
168- ) ,
169- ) ;
170- }
171- } , 30000 ) ; // 30 seconds timeout for file-level tests
172- } ) ;
173-
174- this . ws . send ( JSON . stringify ( data ) ) ;
175- return promise ;
139+ return Promise . race ( [
140+ this . worker . runTest ( data ) ,
141+ new Promise ( ( _ , reject ) =>
142+ setTimeout (
143+ reject ,
144+ 30_000 ,
145+ new Error (
146+ `File test execution timed out for ${ fileItem . uri ! . fsPath } ` ,
147+ ) ,
148+ ) ,
149+ ) , // 30 seconds timeout for file-level tests
150+ ] ) ;
176151 }
177152 }
178153
179154 public async createChildProcess ( ) {
155+ const { cwd, rstestPath } = this . resolveRstestPath ( ) [ 0 ] ;
156+ if ( ! cwd || ! rstestPath ) {
157+ logger . error ( 'Failed to resolve rstest path or cwd' ) ;
158+ return ;
159+ }
160+
180161 const execArgv : string [ ] = [ ] ;
181162 const workerPath = path . resolve ( __dirname , 'worker.js' ) ;
182- const port = await getPort ( ) ;
183- const wsAddress = `ws://localhost:${ port } ` ;
184163 logger . debug ( 'Spawning worker process' , {
185164 workerPath,
186- wsAddress,
187165 } ) ;
188166 const rstestProcess = spawn ( 'node' , [ ...execArgv , workerPath ] , {
189- stdio : 'pipe' ,
167+ cwd,
168+ stdio : [ 'pipe' , 'pipe' , 'pipe' , 'ipc' ] ,
169+ serialization : 'advanced' ,
190170 env : {
191171 ...process . env ,
192172 TEST : 'true' ,
193- RSTEST_WS_ADDRESS : wsAddress ,
194173 } ,
195174 } ) ;
196175
@@ -204,52 +183,20 @@ export class RstestApi {
204183 logger . error ( 'worker stderr' , content . trimEnd ( ) ) ;
205184 } ) ;
206185
207- const server = createServer ( ) . listen ( port ) . unref ( ) ;
208- const wss = new WebSocketServer ( { server } ) ;
186+ this . worker = wrap < Worker > ( rstestProcess ) ;
187+ expose ( rstestProcess , this ) ;
209188
210- wss . once ( 'connection' , ( ws ) => {
211- this . ws = ws ;
212- logger . debug ( 'Worker connected' , { wsAddress } ) ;
213- const { cwd, rstestPath } = this . resolveRstestPath ( ) [ 0 ] ;
214- if ( ! cwd || ! rstestPath ) {
215- logger . error ( 'Failed to resolve rstest path or cwd' ) ;
216- return ;
217- }
218-
219- ws . send (
220- JSON . stringify ( {
221- type : 'init' ,
222- rstestPath,
223- cwd,
224- } ) ,
225- ) ;
226- logger . debug ( 'Sent init payload to worker' , { cwd, rstestPath } ) ;
227-
228- ws . on ( 'message' , ( _data ) => {
229- const _message = JSON . parse ( _data . toString ( ) ) as WorkerEvent ;
230- if ( _message . type === 'finish' ) {
231- const message : WorkerEventFinish = _message ;
232- logger . debug ( 'Received worker completion event' , {
233- id : message . id ,
234- testResult : message . testResults ,
235- testFileResult : message . testFileResults ,
236- } ) ;
237- // Check if we have a pending promise for this test ID
238- const promiseObj = this . testPromises . get ( message . id ) ;
239- if ( promiseObj ) {
240- // Resolve the promise with the message data
241- promiseObj . resolve ( message ) ;
242- // Remove the promise from the map
243- this . testPromises . delete ( message . id ) ;
244- }
245- }
246- } ) ;
247- } ) ;
189+ this . worker . initRstest ( { cwd, rstestPath } ) ;
190+ logger . debug ( 'Sent init payload to worker' , { cwd, rstestPath } ) ;
248191
249192 rstestProcess . on ( 'exit' , ( code , signal ) => {
250193 logger . debug ( 'Worker process exited' , { code, signal } ) ;
251194 } ) ;
252195 }
253196
254197 public async createRstestWorker ( ) { }
198+
199+ async log ( level : LogLevel , message : string ) {
200+ logger [ level ] ( message ) ;
201+ }
255202}
0 commit comments