UNPKG

superchild

Version:

A smarter replacement for node.js's child_process module.

243 lines (220 loc) 8.31 kB
var _ = require('lodash') , assert = require('chai').assert , fmt = require('util').format , path = require('path') , pstatus = require('./pstatus') , spawn = require('../lib/respawn').spawn , superchild = require('../lib/superchild') ; describe('Superchild', function() { describe('basic use cases', function() { it('should get a long directory listing line-by-line', function(cb) { var child = superchild('ls -lh ' + __dirname); var lines = []; child.on('stdout_line', function(lineStr) { lines.push(lineStr); }); child.once('exit', function(code, signal) { assert.equal(0, code, 'ls should terminate with zero exit code'); assert.isAtLeast(lines.length, 3, 'ls should output the correct number of lines'); cb(); }); }); it('should filter and parse LD-JSON objects', function(cb) { var child = superchild('cat'); var objects = []; child.on('json_object', function(jsonObj) { objects.push(jsonObj); child.close(); }); child.send({some: 'data'}); child.once('exit', function(code, signal) { assert.isNotOk(code, 'ls should terminate with zero exit code'); assert.equal(1, objects.length, 'ls should output the correct number of lines'); assert.equal('data', objects[0].some, 'ls should echo data via cat and event json_object'); cb(); }); }); it('should filter and parse LD-JSON arrays', function(cb) { var child = superchild('cat'); var arrays = []; child.on('json_array', function(jsonArr) { arrays.push(jsonArr); child.close(); }); child.send([1, 2, 3]); child.once('exit', function(code, signal) { assert.isNotOk(code, 'ls should terminate with zero exit code'); assert.equal(1, arrays.length, 'ls should output the correct number of lines'); assert.deepEqual([1, 2, 3], arrays[0], 'ls should echo data via cat and event json_array'); cb(); }); }); it('should process the last unterminated line on child exit', function(cb) { var child = superchild('echo -n "sentinel"'); var firstLine; child.once('stdout_line', function(lineStr) { firstLine = lineStr; }); child.once('exit', function() { assert.isOk(firstLine, 'should process the last unterminated line'); assert.equal(firstLine, 'sentinel', 'should echo the correct string'); cb(); }); }); it('should process the last unterminated LD-JSON line on child exit', function(cb) { var child = superchild('echo -n "[123, 456, 789]"'); var firstLine, firstArr; child.once('stdout_line', function(lineStr) { firstLine = lineStr; }); child.once('json_array', function(jsonArr) { firstArr = jsonArr; }); child.once('exit', function() { assert.isNotOk(firstLine, 'should not pass LD-JSON to stdout'); assert.deepEqual(firstArr, [123, 456, 789], 'should parse the correct array'); cb(); }); }); it('should ignore invalid leading and trailing whitespace when parsing LD-JSON', function(cb) { var child = superchild('printf "\x1b \x1f \x10[123, 456, 789] \x07"'); var firstLine, firstArr; child.once('stdout_line', function(lineStr) { firstLine = lineStr; }); child.once('json_array', function(jsonArr) { firstArr = jsonArr; }); child.once('exit', function() { assert.isNotOk(firstLine, 'should not pass LD-JSON to stdout'); assert.deepEqual(firstArr, [123, 456, 789], 'should parse the correct array'); cb(); }); }); // Child should emit exactly one message to each stream and then exit. The // order of received events should match the order of events sent. it('should receive stream events in the correct order', function(cb) { this.slow(2000); this.timeout(4000); var evts = []; var pushEvent = function(evtType) { return function(elem) { evts.push([evtType, elem]); }; }; var child = superchild('node stream-test.js', {cwd: path.join(__dirname, 'programs')}); // Record events received, in order. child.on('stdout_line', pushEvent('stdout_line')); child.on('json_array', pushEvent('json_array')); child.on('json_object', pushEvent('json_object')); child.on('stderr_data', pushEvent('stderr_data')); child.once('exit', function(code, signal) { assert.equal(code, 91, 'should return child exit code'); assert.isNotOk(signal, 'should not be killed with a signal'); assert.deepEqual(evts, [ [ "stdout_line", "This is the multi-line stdout sentinel." ], [ "stdout_line", "\rIt spans three lines." ], [ "stdout_line", "It has Unix and DOS line breaks." ], [ "json_array", [ "this", "is", "the", "json_array", "sentinel" ] ], [ "json_object", { "sentinel": "json_object" } ] ], 'should return child events in the expected order.'); cb(); }); }); }); describe('basic abuse cases', function() { this.slow(3000); this.timeout(5000); // Create a superchild + shell + grandchild node process, // with child and grandchild processes running busy loops. it('should kill nested infinite loops on close()', function(cb) { var child = superchild('node infinite-loop-root.js', { cwd: path.join(__dirname, 'programs') }); setTimeout(function() { assert.isOk(child.pid, 'should eventually have a `child.pid`'); var pGroup = pstatus(child.pid); assert.equal(pGroup.length, 3, 'should spawn 3 processes'); _.forEach(pGroup, function(proc) { assert.isAtLeast(proc.rss, 1, 'should have positive RSS'); assert.isAtLeast(proc.sz, 1, 'should have positive sz'); assert.equal(proc.pgid, child.pid, 'should have pgid == child.pid'); }); child.close(function(err) { assert.isNotOk(err, 'should close without error'); var pGroup = pstatus(child.pid); assert.strictEqual(pGroup.length, 0, 'should kill all children in process tree'); cb(); }); }, 500); }); // Create a superchild that exits with a nonzero error code after a delay. it('should handle delayed abort child programs', function(cb) { var child = superchild('node delayed-abort.js', { cwd: path.join(__dirname, 'programs') }); child.on('exit', function(code, signal) { assert.equal(99, code, 'should return the correct exit code'); cb(); }); }); }); describe('unlogger', function() { this.slow(500); // Ensure that unlogger is exposed, and can be used to implement a simple // two-way echo interaction. it('should be compatible with child.send()', function(cb) { var childOpt = {cwd: path.join(__dirname, 'programs')}; var child = superchild('node echo.js', childOpt); child.send({hello: 'world'}); child.on('json_object', function(obj) { assert.equal(obj.hello, 'world'); child.close(cb); }); }); // Ensure that unlogger is exposed, and can be used to implement a simple // two-way echo interaction. it('should be able to parse "uname -a" output', function(cb) { var child = spawn('uname -a', {shell: true, stdio: 'pipe'}); superchild.unlogger(child.stdout).once('stdout_line', function(unameOutput) { assert.isAtLeast(unameOutput.length, 3); cb(); }); }); }); describe('examples', function() { this.slow(500); it('should run example-1.js', function(cb) { var childOpt = {cwd: path.join(__dirname, 'programs')}; var child = superchild('node example-1.js', childOpt); child.on('exit', function(code) { assert.equal(0, code); cb(); }); }); }); });