1
- import type Mocha from 'mocha' ;
1
+ import Mocha from 'mocha' ;
2
2
import assert from 'assert' ;
3
3
import type {
4
4
ChildProcess ,
@@ -33,49 +33,6 @@ function matches(str: string, pattern: string | RegExp): boolean {
33
33
: pattern . test ( str ) ;
34
34
}
35
35
36
- /**
37
- * Toggle used to ensure an appropriate hook is registered, to clean up test shells.
38
- * NOTE: This is a local variable of the module instead of a static on {@link TestShell} to allow the hooks to toggle it.
39
- */
40
- let testShellEnabled = false ;
41
-
42
- /**
43
- * Enables the TestShell and kill all shells after all tests of the current suite.
44
- */
45
- export function cleanTestShellsAfter ( ) {
46
- before ( 'enabled TestShell' , function ( ) {
47
- assert (
48
- ! testShellEnabled ,
49
- 'TestShell is already enabled, use only one cleanTestShellsAfter or cleanTestShellsAfterEach'
50
- ) ;
51
- testShellEnabled = true ;
52
- } ) ;
53
-
54
- after ( 'kill all TestShell instances' , async function ( this : Mocha . Context ) {
55
- testShellEnabled = false ;
56
- await TestShell . killAll ( ) ;
57
- } ) ;
58
- }
59
-
60
- /**
61
- * Enables the TestShell and kill all shells after each test
62
- * NOTE: This also registers {@link cleanTestShellsAfter} hook internally, to allow `after` hooks to start TestShell instances.
63
- */
64
- export function cleanTestShellsAfterEach ( ) {
65
- cleanTestShellsAfter ( ) ;
66
-
67
- afterEach (
68
- 'kill all TestShell instances' ,
69
- async function ( this : Mocha . Context ) {
70
- assert ( testShellEnabled , 'Expected TestShell to be enabled' ) ;
71
- if ( this . currentTest ?. state === 'failed' ) {
72
- TestShell . printShells ( ) ;
73
- }
74
- await TestShell . killAll ( ) ;
75
- }
76
- ) ;
77
- }
78
-
79
36
export interface TestShellOptions {
80
37
args : string [ ] ;
81
38
env ?: Record < string , string > ;
@@ -89,14 +46,12 @@ export interface TestShellOptions {
89
46
* Test shell helper class.
90
47
*/
91
48
export class TestShell {
92
- private static _openShells : TestShell [ ] = [ ] ;
49
+ private static _openShells : Set < TestShell > = new Set ( ) ;
93
50
51
+ /**
52
+ * @deprecated Use the {@link Mocha.Context.startTestShell} hook instead
53
+ */
94
54
static start ( options : TestShellOptions = { args : [ ] } ) : TestShell {
95
- assert (
96
- testShellEnabled ,
97
- 'Expected TestShell to be enabled, did you call cleanTestShellsAfter or cleanTestShellsAfterEach? Or did you call TestShell.start in an after hook?'
98
- ) ;
99
-
100
55
let shellProcess : ChildProcessWithoutNullStreams ;
101
56
102
57
let env = options . env || process . env ;
@@ -141,7 +96,10 @@ export class TestShell {
141
96
}
142
97
143
98
const shell = new TestShell ( shellProcess , options . consumeStdio ) ;
144
- TestShell . _openShells . push ( shell ) ;
99
+ TestShell . _openShells . add ( shell ) ;
100
+ void shell . waitForExit ( ) . then ( ( ) => {
101
+ TestShell . _openShells . delete ( shell ) ;
102
+ } ) ;
145
103
146
104
return shell ;
147
105
}
@@ -155,17 +113,6 @@ export class TestShell {
155
113
return shell . output ;
156
114
}
157
115
158
- static async killAll ( ) : Promise < void > {
159
- // Using splice to mutate the array of open shells in-place
160
- const openShells = TestShell . _openShells . splice ( 0 ) ;
161
- await Promise . all (
162
- openShells . map ( ( shell ) => {
163
- shell . kill ( ) ;
164
- return shell . waitForExit ( ) ;
165
- } )
166
- ) ;
167
- }
168
-
169
116
debugInformation ( ) {
170
117
return {
171
118
pid : this . process . pid ,
@@ -182,6 +129,21 @@ export class TestShell {
182
129
}
183
130
}
184
131
132
+ static assertNoOpenShells ( ) {
133
+ const debugInformation = [ ...TestShell . _openShells ] . map ( ( shell ) =>
134
+ shell . debugInformation ( )
135
+ ) ;
136
+ assert . strictEqual (
137
+ TestShell . _openShells . size ,
138
+ 0 ,
139
+ `Expected no open shells, found: ${ JSON . stringify (
140
+ debugInformation ,
141
+ null ,
142
+ 2
143
+ ) } `
144
+ ) ;
145
+ }
146
+
185
147
private _process : ChildProcessWithoutNullStreams ;
186
148
187
149
private _output : string ;
@@ -373,3 +335,96 @@ export class TestShell {
373
335
return match . groups ! . logId ;
374
336
}
375
337
}
338
+
339
+ // Context extension to manage TestShell lifetime
340
+
341
+ declare module 'mocha' {
342
+ interface Context {
343
+ /**
344
+ * Starts a test shell and registers a hook to kill it after the test
345
+ */
346
+ startTestShell ( options ?: TestShellOptions ) : TestShell ;
347
+ }
348
+ }
349
+
350
+ const TEST_SHELLS_AFTER_ALL = Symbol ( 'test-shells-after-all' ) ;
351
+ const TEST_SHELLS_AFTER_EACH = Symbol ( 'test-shells-after-each' ) ;
352
+
353
+ type AfterAllInjectedSuite = {
354
+ [ TEST_SHELLS_AFTER_ALL ] : Set < TestShell > ;
355
+ } ;
356
+
357
+ type AfterEachInjectedSuite = {
358
+ [ TEST_SHELLS_AFTER_EACH ] : Set < TestShell > ;
359
+ } ;
360
+
361
+ /**
362
+ * Registers an after (all or each) hook to kill test shells started during the hooks or tests
363
+ */
364
+ function ensureAfterHook (
365
+ hookName : 'afterEach' ,
366
+ suite : Mocha . Suite
367
+ ) : asserts suite is AfterEachInjectedSuite & Mocha . Suite ;
368
+ function ensureAfterHook (
369
+ hookName : 'afterAll' ,
370
+ suite : Mocha . Suite
371
+ ) : asserts suite is AfterAllInjectedSuite & Mocha . Suite ;
372
+ function ensureAfterHook (
373
+ hookName : 'afterEach' | 'afterAll' ,
374
+ suite : Partial < AfterAllInjectedSuite & AfterEachInjectedSuite > & Mocha . Suite
375
+ ) : void {
376
+ const symbol =
377
+ hookName === 'afterAll' ? TEST_SHELLS_AFTER_ALL : TEST_SHELLS_AFTER_EACH ;
378
+ if ( ! suite [ symbol ] ) {
379
+ // Store the set of shells to kill afterwards
380
+ const shells = new Set < TestShell > ( ) ;
381
+ suite [ symbol ] = shells ;
382
+ suite [ hookName ] ( async ( ) => {
383
+ const shellsToKill = [ ...shells ] ;
384
+ shells . clear ( ) ;
385
+ await Promise . all (
386
+ shellsToKill . map ( ( shell ) => {
387
+ // TODO: Consider if it's okay to kill those that are already killed?
388
+ shell . kill ( ) ;
389
+ return shell . waitForExit ( ) ;
390
+ } )
391
+ ) ;
392
+ } ) ;
393
+ }
394
+ }
395
+
396
+ Mocha . Context . prototype . startTestShell = function (
397
+ this : Mocha . Context ,
398
+ options : TestShellOptions
399
+ ) {
400
+ const { test : runnable } = this ;
401
+ assert ( runnable , 'Expected a runnable / test' ) ;
402
+ const { parent } = runnable ;
403
+ assert ( parent , 'Expected runnable to have a parent' ) ;
404
+ // Start the shell
405
+ const shell = TestShell . start ( options ) ;
406
+ // Register a hook to kill the shell
407
+ if ( runnable instanceof Mocha . Hook ) {
408
+ if (
409
+ runnable . originalTitle === '"before each" hook' ||
410
+ runnable . originalTitle === '"after each" hook'
411
+ ) {
412
+ ensureAfterHook ( 'afterEach' , parent ) ;
413
+ parent [ TEST_SHELLS_AFTER_EACH ] . add ( shell ) ;
414
+ } else if (
415
+ runnable . originalTitle === '"before all" hook' ||
416
+ runnable . originalTitle === '"after all" hook'
417
+ ) {
418
+ ensureAfterHook ( 'afterAll' , parent ) ;
419
+ parent [ TEST_SHELLS_AFTER_ALL ] . add ( shell ) ;
420
+ } else {
421
+ throw new Error ( `Unexpected ${ runnable . originalTitle || runnable . title } ` ) ;
422
+ }
423
+ } else if ( runnable instanceof Mocha . Test ) {
424
+ ensureAfterHook ( 'afterEach' , parent ) ;
425
+ parent [ TEST_SHELLS_AFTER_EACH ] . add ( shell ) ;
426
+ } else {
427
+ throw new Error ( 'Unexpected Runnable: Expected a Hook or a Test' ) ;
428
+ }
429
+ return shell ;
430
+ } ;
0 commit comments