-
-
Save zorrodg/c349cf54a3f6d0a9ba62e0f4066f31cb to your computer and use it in GitHub Desktop.
/** | |
* 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' | |
}; |
MIT License | |
Copyright © 2019 Andrés Zorro | |
Permission is hereby granted, free of charge, to any person obtaining a copy | |
of this software and associated documentation files (the "Software"), to deal | |
in the Software without restriction, including without limitation the rights | |
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
copies of the Software, and to permit persons to whom the Software is | |
furnished to do so, subject to the following conditions: | |
The above copyright notice and this permission notice shall be included in all | |
copies or substantial portions of the Software. | |
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
SOFTWARE. |
How can I change it to use some npm globally installed library?
I didn't understand this question. Can you please describe your use case?
@zorrodg If I understood what @rluvaton wrote, he was asking about how he could turn that into a package that could be globally installed (i.e. via
npm i -g
).@rluvaton I haven't worked on such package (yet) but I've got that on my list, we could collaborate on this (+ anyone else interested) if you want?
@Berkmann18 We can, create a repo and add me as a collaborator
@Berkmann18 We can, create a repo and add me as a collaborator
Done, there's a bit more of work to do on it so it can be deployed but there's a start.
@Berkmann18 We can, create a repo and add me as a collaborator
Done, there's a bit more of work to do on it so it can be deployed but there's a start.
👍🏻 I’ll try to work on this tomorrow
Amazing guys, thanks for taking the stab at it. Like I said, I’m really not interested in covering all edge cases in an npm module, but whatever works for you!
Thought I'd share a snippet that I've used in my tests. It's a bit shorter and uses output from stdout
to trigger processing of the next batch of inputs. I ran the utility without setTimeout and it worked even for consecutive input operations. setTimeout is still used, but more as a precaution with a time buffer of just 5 milliseconds. Realistic interactive CLI tests are slower than normal and using very short timeouts speeds things up.
https://github.com/aptivator/anagrammer/blob/master/test/_lib/cli.js
@zorrodg If I understood what @rluvaton wrote, he was asking about how he could turn that into a package that could be globally installed (i.e. via
npm i -g
).@rluvaton I haven't worked on such package (yet) but I've got that on my list, we could collaborate on this (+ anyone else interested) if you want?