-
-
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. |
@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
Done, there's a bit more of work to do on it so it can be deployed but there's a start.