1
- import type { ChildProcess } from 'child_process' ;
2
- import { spawn } from 'child_process' ;
1
+ import { spawn , spawnSync } from 'child_process' ;
3
2
import { join } from 'path' ;
4
3
import type { Envelope , EnvelopeItemType , Event , SerializedSession } from '@sentry/types' ;
5
4
import axios from 'axios' ;
@@ -30,14 +29,17 @@ export function assertSentryTransaction(actual: Event, expected: Partial<Event>)
30
29
} ) ;
31
30
}
32
31
33
- const CHILD_PROCESSES = new Set < ChildProcess > ( ) ;
32
+ const CLEANUP_STEPS = new Set < VoidFunction > ( ) ;
34
33
35
34
export function cleanupChildProcesses ( ) : void {
36
- for ( const child of CHILD_PROCESSES ) {
37
- child . kill ( ) ;
35
+ for ( const step of CLEANUP_STEPS ) {
36
+ step ( ) ;
38
37
}
38
+ CLEANUP_STEPS . clear ( ) ;
39
39
}
40
40
41
+ process . on ( 'exit' , cleanupChildProcesses ) ;
42
+
41
43
/** Promise only resolves when fn returns true */
42
44
async function waitFor ( fn : ( ) => boolean , timeout = 10_000 ) : Promise < void > {
43
45
let remaining = timeout ;
@@ -50,6 +52,58 @@ async function waitFor(fn: () => boolean, timeout = 10_000): Promise<void> {
50
52
}
51
53
}
52
54
55
+ type VoidFunction = ( ) => void ;
56
+
57
+ interface DockerOptions {
58
+ /**
59
+ * The working directory to run docker compose in
60
+ */
61
+ workingDirectory : string [ ] ;
62
+ /**
63
+ * The strings to look for in the output to know that the docker compose is ready for the test to be run
64
+ */
65
+ readyMatches : string [ ] ;
66
+ }
67
+
68
+ /**
69
+ * Runs docker compose up and waits for the readyMatches to appear in the output
70
+ *
71
+ * Returns a function that can be called to docker compose down
72
+ */
73
+ async function runDockerCompose ( options : DockerOptions ) : Promise < VoidFunction > {
74
+ return new Promise ( ( resolve , reject ) => {
75
+ const cwd = join ( ...options . workingDirectory ) ;
76
+ const close = ( ) : void => {
77
+ spawnSync ( 'docker' , [ 'compose' , 'down' , '--volumes' ] , { cwd } ) ;
78
+ } ;
79
+
80
+ // ensure we're starting fresh
81
+ close ( ) ;
82
+
83
+ const child = spawn ( 'docker' , [ 'compose' , 'up' ] , { cwd } ) ;
84
+
85
+ const timeout = setTimeout ( ( ) => {
86
+ close ( ) ;
87
+ reject ( new Error ( 'Timed out waiting for docker-compose' ) ) ;
88
+ } , 60_000 ) ;
89
+
90
+ function newData ( data : Buffer ) : void {
91
+ const text = data . toString ( 'utf8' ) ;
92
+
93
+ for ( const match of options . readyMatches ) {
94
+ if ( text . includes ( match ) ) {
95
+ child . stdout . removeAllListeners ( ) ;
96
+ clearTimeout ( timeout ) ;
97
+ resolve ( close ) ;
98
+ }
99
+ }
100
+ }
101
+
102
+ child . stdout . on ( 'data' , newData ) ;
103
+ child . stderr . on ( 'data' , newData ) ;
104
+ } ) ;
105
+ }
106
+
53
107
type Expected =
54
108
| {
55
109
event : Partial < Event > | ( ( event : Event ) => void ) ;
@@ -70,6 +124,7 @@ export function createRunner(...paths: string[]) {
70
124
const flags : string [ ] = [ ] ;
71
125
const ignored : EnvelopeItemType [ ] = [ ] ;
72
126
let withSentryServer = false ;
127
+ let dockerOptions : DockerOptions | undefined ;
73
128
let ensureNoErrorOutput = false ;
74
129
75
130
if ( testPath . endsWith ( '.ts' ) ) {
@@ -93,6 +148,10 @@ export function createRunner(...paths: string[]) {
93
148
ignored . push ( ...types ) ;
94
149
return this ;
95
150
} ,
151
+ withDockerCompose : function ( options : DockerOptions ) {
152
+ dockerOptions = options ;
153
+ return this ;
154
+ } ,
96
155
ensureNoErrorOutput : function ( ) {
97
156
ensureNoErrorOutput = true ;
98
157
return this ;
@@ -182,80 +241,94 @@ export function createRunner(...paths: string[]) {
182
241
? createBasicSentryServer ( newEnvelope )
183
242
: Promise . resolve ( undefined ) ;
184
243
244
+ const dockerStartup : Promise < VoidFunction | undefined > = dockerOptions
245
+ ? runDockerCompose ( dockerOptions )
246
+ : Promise . resolve ( undefined ) ;
247
+
248
+ const startup = Promise . all ( [ dockerStartup , serverStartup ] ) ;
249
+
185
250
// eslint-disable-next-line @typescript-eslint/no-floating-promises
186
- serverStartup . then ( mockServerPort => {
187
- const env = mockServerPort
188
- ? { ...process . env , SENTRY_DSN : `http://public@localhost:${ mockServerPort } /1337` }
189
- : process . env ;
251
+ startup
252
+ . then ( ( [ dockerChild , mockServerPort ] ) => {
253
+ if ( dockerChild ) {
254
+ CLEANUP_STEPS . add ( dockerChild ) ;
255
+ }
190
256
191
- // eslint-disable-next-line no-console
192
- if ( process . env . DEBUG ) console . log ( 'starting scenario' , testPath , flags , env . SENTRY_DSN ) ;
257
+ const env = mockServerPort
258
+ ? { ...process . env , SENTRY_DSN : `http://public@localhost:${ mockServerPort } /1337` }
259
+ : process . env ;
193
260
194
- child = spawn ( 'node' , [ ...flags , testPath ] , { env } ) ;
261
+ // eslint-disable-next-line no-console
262
+ if ( process . env . DEBUG ) console . log ( 'starting scenario' , testPath , flags , env . SENTRY_DSN ) ;
195
263
196
- CHILD_PROCESSES . add ( child ) ;
264
+ child = spawn ( 'node' , [ ... flags , testPath ] , { env } ) ;
197
265
198
- if ( ensureNoErrorOutput ) {
199
- child . stderr . on ( 'data' , ( data : Buffer ) => {
200
- const output = data . toString ( ) ;
201
- complete ( new Error ( `Expected no error output but got: '${ output } '` ) ) ;
266
+ CLEANUP_STEPS . add ( ( ) => {
267
+ child ?. kill ( ) ;
202
268
} ) ;
203
- }
204
-
205
- child . on ( 'close' , ( ) => {
206
- hasExited = true ;
207
269
208
270
if ( ensureNoErrorOutput ) {
209
- complete ( ) ;
271
+ child . stderr . on ( 'data' , ( data : Buffer ) => {
272
+ const output = data . toString ( ) ;
273
+ complete ( new Error ( `Expected no error output but got: '${ output } '` ) ) ;
274
+ } ) ;
210
275
}
211
- } ) ;
212
276
213
- // Pass error to done to end the test quickly
214
- child . on ( 'error' , e => {
215
- // eslint-disable-next-line no-console
216
- if ( process . env . DEBUG ) console . log ( 'scenario error' , e ) ;
217
- complete ( e ) ;
218
- } ) ;
219
-
220
- function tryParseEnvelopeFromStdoutLine ( line : string ) : void {
221
- // Lines can have leading '[something] [{' which we need to remove
222
- const cleanedLine = line . replace ( / ^ .* ?] \[ { " / , '[{"' ) ;
223
-
224
- // See if we have a port message
225
- if ( cleanedLine . startsWith ( '{"port":' ) ) {
226
- const { port } = JSON . parse ( cleanedLine ) as { port : number } ;
227
- scenarioServerPort = port ;
228
- return ;
229
- }
277
+ child . on ( 'close' , ( ) => {
278
+ hasExited = true ;
230
279
231
- // Skip any lines that don't start with envelope JSON
232
- if ( ! cleanedLine . startsWith ( '[{' ) ) {
233
- return ;
234
- }
280
+ if ( ensureNoErrorOutput ) {
281
+ complete ( ) ;
282
+ }
283
+ } ) ;
235
284
236
- try {
237
- const envelope = JSON . parse ( cleanedLine ) as Envelope ;
238
- newEnvelope ( envelope ) ;
239
- } catch ( _ ) {
240
- //
241
- }
242
- }
285
+ // Pass error to done to end the test quickly
286
+ child . on ( 'error' , e => {
287
+ // eslint-disable-next-line no-console
288
+ if ( process . env . DEBUG ) console . log ( 'scenario error' , e ) ;
289
+ complete ( e ) ;
290
+ } ) ;
243
291
244
- let buffer = Buffer . alloc ( 0 ) ;
245
- child . stdout . on ( 'data' , ( data : Buffer ) => {
246
- // This is horribly memory inefficient but it's only for tests
247
- buffer = Buffer . concat ( [ buffer , data ] ) ;
292
+ function tryParseEnvelopeFromStdoutLine ( line : string ) : void {
293
+ // Lines can have leading '[something] [{' which we need to remove
294
+ const cleanedLine = line . replace ( / ^ .* ?] \[ { " / , '[{"' ) ;
248
295
249
- let splitIndex = - 1 ;
250
- while ( ( splitIndex = buffer . indexOf ( 0xa ) ) >= 0 ) {
251
- const line = buffer . subarray ( 0 , splitIndex ) . toString ( ) ;
252
- buffer = Buffer . from ( buffer . subarray ( splitIndex + 1 ) ) ;
253
- // eslint-disable-next-line no-console
254
- if ( process . env . DEBUG ) console . log ( 'line' , line ) ;
255
- tryParseEnvelopeFromStdoutLine ( line ) ;
296
+ // See if we have a port message
297
+ if ( cleanedLine . startsWith ( '{"port":' ) ) {
298
+ const { port } = JSON . parse ( cleanedLine ) as { port : number } ;
299
+ scenarioServerPort = port ;
300
+ return ;
301
+ }
302
+
303
+ // Skip any lines that don't start with envelope JSON
304
+ if ( ! cleanedLine . startsWith ( '[{' ) ) {
305
+ return ;
306
+ }
307
+
308
+ try {
309
+ const envelope = JSON . parse ( cleanedLine ) as Envelope ;
310
+ newEnvelope ( envelope ) ;
311
+ } catch ( _ ) {
312
+ //
313
+ }
256
314
}
257
- } ) ;
258
- } ) ;
315
+
316
+ let buffer = Buffer . alloc ( 0 ) ;
317
+ child . stdout . on ( 'data' , ( data : Buffer ) => {
318
+ // This is horribly memory inefficient but it's only for tests
319
+ buffer = Buffer . concat ( [ buffer , data ] ) ;
320
+
321
+ let splitIndex = - 1 ;
322
+ while ( ( splitIndex = buffer . indexOf ( 0xa ) ) >= 0 ) {
323
+ const line = buffer . subarray ( 0 , splitIndex ) . toString ( ) ;
324
+ buffer = Buffer . from ( buffer . subarray ( splitIndex + 1 ) ) ;
325
+ // eslint-disable-next-line no-console
326
+ if ( process . env . DEBUG ) console . log ( 'line' , line ) ;
327
+ tryParseEnvelopeFromStdoutLine ( line ) ;
328
+ }
329
+ } ) ;
330
+ } )
331
+ . catch ( e => complete ( e ) ) ;
259
332
260
333
return {
261
334
childHasExited : function ( ) : boolean {
0 commit comments