Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

'This socket is closed' when stdout is listened in Mocha test

This test

it.only('should not throw', () => {
    var output = '';

    function callback(data) {
        output += data.toString();
    }

    process.stdout.on('data', callback); // error is thrown at this line
    // ...
    process.stdout.removeListener('data', callback);
})

throws an error:

Error: This socket is closed                                                                           
    at WriteStream.Socket._writeGeneric (net.js:679:19)                                                
    at WriteStream.Socket._write (net.js:730:8)                                                        
    at doWrite (_stream_writable.js:331:12)                                                            
    at writeOrBuffer (_stream_writable.js:317:5)                                                       
    at WriteStream.Writable.write (_stream_writable.js:243:11)                                         
    at WriteStream.Socket.write (net.js:657:40)                                                        
    at Console.log (console.js:43:16)                                                                  
    at Runner.<anonymous> (node_modules\mocha\lib\reporters\spec.js:80:13)
    at emitTwo (events.js:111:20)                                                                      
    at Runner.emit (events.js:191:7)                                                                   
    at Runner.fail (node_modules\mocha\lib\runner.js:251:8)               
    at Runner.uncaught (node_modules\mocha\lib\runner.js:757:8)           
    at process.uncaught (node_modules\mocha\lib\runner.js:839:10)         
    at emitOne (events.js:96:13)                                                                       
    at process.emit (events.js:188:7)                                                                  
    at process._fatalException (bootstrap_node.js:297:26)                                              

Where node_modules\mocha\lib\reporters\spec.js:80:13 are these Mocha lines:

  runner.on('fail', function(test) {
    console.log(indent() + color('fail', '  %d) %s'), ++n, test.title);
  });

It's supposed to test code that outputs to process.stdout with spawn, but I wasn't able to get to this part; the error is thrown instantly on process.stdout.on('data', ...) call.

The problem persists with latest Mocha (5.2.0) and default configuration, a reporter in use doesn't affect the result.

What is going on and how can process.stdout be listened? If this is impossible, how can stdout from spawned process be tested in Mocha otherwise?

like image 394
Estus Flask Avatar asked Jun 17 '18 05:06

Estus Flask


1 Answers

I was not able to reproduce the error: process.stdout.on('data', callback); // error is thrown at this line. So hard to say why it breaks for you. Anyway process.stdout is a writeable stream, so data event is not supported. On the other hand childProcess.stdout is a readable stream from parent perspective.

The easiest way to intercept stdout of a child process is to:

const ch = spawn(...)
ch.stdout.on('data', d => ...) // 'd' is a buffer

If you want to test against stdout of an arbitrary spawn process I am afraid you need to pass a custom writeable stream as subprocess's stdout, when spawning the process. The stream needs to be based on a file descriptor, so simple Duplex will not work unfortunately.

Here is a possible setup:

process.js

const { spawn } = require('child_process')

module.exports = (outStream) => {
  console.log('PARENT')
  const s = spawn('node', [`${__dirname}/child.js`], { stdio: ['ignore', outStream] })

  return () => {
    console.log('PARENT-DISPOSE')
    s.kill()
  }
}

child.js

process.stdout.write('FROM CHILD')
setTimeout(() => process.stdout.write('FROM CHILD END'), 100)

process.spec.js

const { spawn } = require('child_process')
const fs = require('fs')

const process = require('./process.js')

// path to temporary file
const commF = `${__dirname}/comm`

it('test spawn', (done) => {
  const w = fs.createWriteStream(commF)
  let dispose

  w.on('open', () => {
    dispose = process(w)
  })

  // unfortunately I was not able to make fs.createReadStram to read data in real-time
  // so here we use another subprocess just for that
  let readProcess = spawn('tail', ['-f', commF])

  readProcess.stdout.on('data', x => {
    const d = x && x.toString()
    console.log('TEST', d) // only logs from child are present here
    if (d.startsWith('FROM CHILD END')) {
      w.close()
      fs.unlinkSync(commF)
      dispose()
      readProcess.kill()
      done()
    }
  })
}).timeout(500)

Not saying that this is the best way to do it: it could be abstracted away for re-usability and the tail dependency may be removed.

like image 114
artur grzesiak Avatar answered Nov 06 '22 04:11

artur grzesiak