-
-
Save adam-edison/189d1969c249571692eec5da66663beb to your computer and use it in GitHub Desktop.
CLI Integration Test Helper
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Integration test helper | |
* Author: Andrés Zorro <[email protected]> | |
*/ | |
const { existsSync } = require('fs'); | |
const { constants } = require('os'); | |
const spawn = require('cross-spawn'); | |
const concat = require('concat-stream'); | |
const PATH = process.env.PATH; | |
/** | |
* Creates a child process with script path | |
* @param {string} processPath Path of the process to execute | |
* @param {Array} args Arguments to the command | |
* @param {Object} env (optional) Environment variables | |
*/ | |
function createProcess(processPath, args = [], env = null) { | |
// Ensure that path exists | |
if (!processPath || !existsSync(processPath)) { | |
throw new Error('Invalid process path'); | |
} | |
args = [processPath].concat(args); | |
// This works for node based CLIs, but can easily be adjusted to | |
// any other process installed in the system | |
return spawn('node', args, { | |
env: Object.assign( | |
{ | |
NODE_ENV: 'test', | |
preventAutoStart: false, | |
PATH // This is needed in order to get all the binaries in your current terminal | |
}, | |
env | |
), | |
stdio: [null, null, null, 'ipc'] // This enables interprocess communication (IPC) | |
}); | |
} | |
/** | |
* Creates a command and executes inputs (user responses) to the stdin | |
* Returns a promise that resolves when all inputs are sent | |
* Rejects the promise if any error | |
* @param {string} processPath Path of the process to execute | |
* @param {Array} args Arguments to the command | |
* @param {Array} inputs (Optional) Array of inputs (user responses) | |
* @param {Object} opts (optional) Environment variables | |
*/ | |
function executeWithInput(processPath, args = [], inputs = [], opts = {}) { | |
if (!Array.isArray(inputs)) { | |
opts = inputs; | |
inputs = []; | |
} | |
const { env = null, timeout = 100, maxTimeout = 10000 } = opts; | |
const childProcess = createProcess(processPath, args, env); | |
childProcess.stdin.setEncoding('utf-8'); | |
let currentInputTimeout, killIOTimeout; | |
// Creates a loop to feed user inputs to the child process in order to get results from the tool | |
// This code is heavily inspired (if not blantantly copied) from inquirer-test: | |
// https://github.com/ewnd9/inquirer-test/blob/6e2c40bbd39a061d3e52a8b1ee52cdac88f8d7f7/index.js#L14 | |
const loop = inputs => { | |
if (killIOTimeout) { | |
clearTimeout(killIOTimeout); | |
} | |
if (!inputs.length) { | |
childProcess.stdin.end(); | |
// Set a timeout to wait for CLI response. If CLI takes longer than | |
// maxTimeout to respond, kill the childProcess and notify user | |
killIOTimeout = setTimeout(() => { | |
console.error('Error: Reached I/O timeout'); | |
childProcess.kill(constants.signals.SIGTERM); | |
}, maxTimeout); | |
return; | |
} | |
currentInputTimeout = setTimeout(() => { | |
childProcess.stdin.write(inputs[0]); | |
// Log debug I/O statements on tests | |
if (env && env.DEBUG) { | |
console.log('input:', inputs[0]); | |
} | |
loop(inputs.slice(1)); | |
}, timeout); | |
}; | |
const promise = new Promise((resolve, reject) => { | |
// Get errors from CLI | |
childProcess.stderr.on('data', data => { | |
// Log debug I/O statements on tests | |
if (env && env.DEBUG) { | |
console.log('error:', data.toString()); | |
} | |
}); | |
// Get output from CLI | |
childProcess.stdout.on('data', data => { | |
// Log debug I/O statements on tests | |
if (env && env.DEBUG) { | |
console.log('output:', data.toString()); | |
} | |
}); | |
childProcess.stderr.once('data', err => { | |
childProcess.stdin.end(); | |
if (currentInputTimeout) { | |
clearTimeout(currentInputTimeout); | |
inputs = []; | |
} | |
reject(err.toString()); | |
}); | |
childProcess.on('error', reject); | |
// Kick off the process | |
loop(inputs); | |
childProcess.stdout.pipe( | |
concat(result => { | |
if (killIOTimeout) { | |
clearTimeout(killIOTimeout); | |
} | |
resolve(result.toString()); | |
}) | |
); | |
}); | |
// Appending the process to the promise, in order to | |
// add additional parameters or behavior (such as IPC communication) | |
promise.attachedProcess = childProcess; | |
return promise; | |
} | |
module.exports = { | |
createProcess, | |
create: processPath => { | |
const fn = (...args) => executeWithInput(processPath, ...args); | |
return { | |
execute: fn | |
}; | |
}, | |
DOWN: '\x1B\x5B\x42', | |
UP: '\x1B\x5B\x41', | |
ENTER: '\x0D', | |
SPACE: '\x20' | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment