33const assert = require ( 'assert' ) ;
44const path = require ( 'path' ) ;
55const { access } = require ( 'node:fs/promises' ) ;
6+ const { spawn } = require ( 'child_process' ) ;
7+ const { EOL } = require ( 'os' ) ;
8+ const readline = require ( 'readline' ) ;
69
710const noop = ( ) => { } ;
811
@@ -26,7 +29,7 @@ function runCallChecks (exitCode) {
2629 context . name ,
2730 context . messageSegment ,
2831 context . actual ) ;
29- console . log ( context . stack . split ( '\n' ) . slice ( 2 ) . join ( '\n' ) ) ;
32+ console . log ( context . stack . split ( EOL ) . slice ( 2 ) . join ( EOL ) ) ;
3033 } ) ;
3134
3235 if ( failed . length ) process . exit ( 1 ) ;
@@ -190,3 +193,60 @@ exports.runTestWithBuildType = async function (test, buildType) {
190193 await Promise . resolve ( test ( buildType ) )
191194 . finally ( exports . mustCall ( ) ) ;
192195} ;
196+
197+ // Some tests have to run in their own process, otherwise they would interfere
198+ // with each other. Such tests export a factory function rather than the test
199+ // itself so as to avoid automatic instantiation, and therefore interference,
200+ // in the main process. Two examples are addon and addon_data, both of which
201+ // use Napi::Env::SetInstanceData(). This helper function provides a common
202+ // approach for running such tests.
203+ exports . runTestInChildProcess = function ( { suite, testName, expectedStderr } ) {
204+ return exports . runTestWithBindingPath ( ( bindingName ) => {
205+ return new Promise ( ( resolve ) => {
206+ bindingName = bindingName . split ( '\\' ) . join ( '\\\\' ) ;
207+ // Test suites are assumed to be located here.
208+ const suitePath = path . join ( __dirname , '..' , 'child_processes' , suite ) ;
209+ const child = spawn ( process . execPath , [
210+ '--expose-gc' ,
211+ '-e' ,
212+ `require('${ suitePath } ').${ testName } (require('${ bindingName } '))`
213+ ] ) ;
214+ let eventCount = 0 ;
215+ const resultOfProcess = { stderr : [ ] } ;
216+ const maybeResolve = ( partialResult = { } ) => {
217+ Object . assign ( resultOfProcess , partialResult ) ;
218+ if ( ++ eventCount === 2 ) {
219+ resolve ( resultOfProcess ) ;
220+ }
221+ } ;
222+
223+ // Capture the exit code and signal.
224+ child
225+ . on ( 'exit' , ( code , signal ) => maybeResolve ( { code, signal } ) )
226+ . on ( 'close' , ( ) => maybeResolve ( ) ) ;
227+
228+ // Capture the stderr as an array of lines.
229+ readline
230+ . createInterface ( { input : child . stderr } )
231+ . on ( 'line' , ( line ) => {
232+ resultOfProcess . stderr . push ( line ) ;
233+ } ) ;
234+ } ) . then ( actual => {
235+ // Back up the stderr in case the assertion fails.
236+ const fullStderr = actual . stderr . map ( item => `from child process: ${ item } ` ) ;
237+ const expected = { stderr : expectedStderr , code : 0 , signal : null } ;
238+
239+ if ( ! expectedStderr ) {
240+ // If we don't care about stderr, delete it.
241+ delete actual . stderr ;
242+ delete expected . stderr ;
243+ } else {
244+ // Otherwise we only care about expected lines in the actual stderr, so
245+ // filter out everything else.
246+ actual . stderr = actual . stderr . filter ( line => expectedStderr . includes ( line ) ) ;
247+ }
248+
249+ assert . deepStrictEqual ( actual , expected , `Assertion for child process test ${ suite } .${ testName } failed:${ EOL } ` + fullStderr . join ( EOL ) ) ;
250+ } ) ;
251+ } ) ;
252+ } ;
0 commit comments