UNPKG

dredd

Version:

HTTP API Testing Framework

377 lines (315 loc) 11.8 kB
const console = require('console'); // Stubbed in tests by proxyquire const fs = require('fs'); const optimist = require('optimist'); const os = require('os'); const spawnArgs = require('spawn-args'); const spawnSync = require('cross-spawn').sync; const configUtils = require('./configUtils'); const Dredd = require('./Dredd'); const ignorePipeErrors = require('./ignorePipeErrors'); const interactiveConfig = require('./init'); const logger = require('./logger'); const { applyLoggingOptions } = require('./configuration'); const { spawn } = require('./childProcess'); const packageData = require('../package.json'); class CLI { constructor(options = {}, cb) { this.cb = cb; this.finished = false; this.exit = options.exit; this.custom = options.custom || {}; this.setExitOrCallback(); if (!this.custom.cwd || typeof this.custom.cwd !== 'string') { this.custom.cwd = process.cwd(); } if (!this.custom.argv || !Array.isArray(this.custom.argv)) { this.custom.argv = []; } } setOptimistArgv() { this.optimist = optimist(this.custom.argv, this.custom.cwd); this.cliArgv = this.optimist.argv; this.optimist.usage(`\ Usage: $ dredd init Or: $ dredd <path or URL to API description document> <URL of tested server> [OPTIONS] Example: $ dredd ./api-description.apib http://127.0.0.1:3000 --dry-run\ `) .options(Dredd.options) .wrap(80); this.argv = this.optimist.argv; applyLoggingOptions(this.argv); } // Gracefully terminate server stopServer(callback) { if (!this.serverProcess || !this.serverProcess.spawned) { logger.debug('No backend server process to terminate.'); return callback(); } if (this.serverProcess.terminated) { logger.debug('The backend server process has already terminated'); return callback(); } logger.debug('Terminating backend server process, PID', this.serverProcess.pid); this.serverProcess.terminate({ force: true }); this.serverProcess.on('exit', () => callback()); } // This thing-a-ma-bob here is only for purpose of testing // It's basically a dependency injection for the process.exit function setExitOrCallback() { if (!this.cb) { if (this.exit && this.exit === process.exit) { this.sigIntEventAdd = true; } if (this.exit) { this._processExit = (exitStatus) => { logger.debug(`Using configured custom exit() method to terminate the Dredd process with status '${exitStatus}'.`); this.finished = true; this.stopServer(() => { this.exit(exitStatus); }); }; } else { this._processExit = (exitStatus) => { logger.debug(`Using native process.exit() method to terminate the Dredd process with status '${exitStatus}'.`); this.stopServer(() => process.exit(exitStatus)); }; } } else { this._processExit = (exitStatus) => { logger.debug(`Using configured custom callback to terminate the Dredd process with status '${exitStatus}'.`); this.finished = true; if (this.sigIntEventAdded) { if (this.serverProcess && !this.serverProcess.terminated) { logger.debug('Killing backend server process before Dredd exits.'); this.serverProcess.signalKill(); } process.removeEventListener('SIGINT', this.commandSigInt); } this.cb(exitStatus); return this; }; } } moveBlueprintArgToPath() { // Transform path and p argument to array if it's not if (!Array.isArray(this.argv.path)) { this.argv.path = this.argv.p = [this.argv.path]; } } checkRequiredArgs() { let argError = false; // If 'blueprint' is missing if (!this.argv._[0]) { console.error('\nError: Must specify path to API description document.'); argError = true; } // If 'endpoint' is missing if (!this.argv._[1]) { console.error('\nError: Must specify URL of the tested API instance.'); argError = true; } // Show help if argument is missing if (argError) { console.error('\n'); this.optimist.showHelp(console.error); this._processExit(1); } } runExitingActions() { // Run interactive config if (this.argv._[0] === 'init' || this.argv.init === true) { logger.debug('Starting interactive configuration.'); this.finished = true; interactiveConfig(this.argv, (config) => { configUtils.save(config); }, (err) => { if (err) { logger.error('Could not configure Dredd', err); } this._processExit(0); }); // Show help } else if (this.argv.help === true) { this.optimist.showHelp(console.error); this._processExit(0); // Show version } else if (this.argv.version === true) { console.log(`\ ${packageData.name} v${packageData.version} \ (${os.type()} ${os.release()}; ${os.arch()})\ `); this._processExit(0); } } loadDreddFile() { const configPath = this.argv.config; logger.debug('Loading configuration file:', configPath); if (configPath && fs.existsSync(configPath)) { logger.debug(`Configuration '${configPath}' found, ignoring other arguments.`); this.argv = configUtils.load(configPath); } // Overwrite saved config with cli arguments Object.keys(this.cliArgv).forEach((key) => { const value = this.cliArgv[key]; if (key !== '_' && key !== '$0') { this.argv[key] = value; } }); applyLoggingOptions(this.argv); } parseCustomConfig() { this.argv.custom = configUtils.parseCustom(this.argv.custom); } runServerAndThenDredd() { if (!this.argv.server) { logger.debug('No backend server process specified, starting testing at once'); this.runDredd(this.dreddInstance); } else { logger.debug('Backend server process specified, starting backend server and then testing'); const parsedArgs = spawnArgs(this.argv.server); const command = parsedArgs.shift(); logger.debug(`Using '${command}' as a server command, ${JSON.stringify(parsedArgs)} as arguments`); this.serverProcess = spawn(command, parsedArgs); logger.debug(`Starting backend server process with command: ${this.argv.server}`); this.serverProcess.stdout.setEncoding('utf8'); this.serverProcess.stdout.on('data', data => process.stdout.write(data.toString())); this.serverProcess.stderr.setEncoding('utf8'); this.serverProcess.stderr.on('data', data => process.stdout.write(data.toString())); this.serverProcess.on('signalTerm', () => logger.debug('Gracefully terminating the backend server process')); this.serverProcess.on('signalKill', () => logger.debug('Killing the backend server process')); this.serverProcess.on('crash', (exitStatus, killed) => { if (killed) { logger.debug('Backend server process was killed'); } }); this.serverProcess.on('exit', () => { logger.debug('Backend server process exited'); }); this.serverProcess.on('error', (err) => { logger.error('Command to start backend server process failed, exiting Dredd', err); this._processExit(1); }); // Ensure server is not running when dredd exits prematurely somewhere process.on('beforeExit', () => { if (this.serverProcess && !this.serverProcess.terminated) { logger.debug('Killing backend server process before Dredd exits'); this.serverProcess.signalKill(); } }); // Ensure server is not running when dredd exits prematurely somewhere process.on('exit', () => { if (this.serverProcess && !this.serverProcess.terminated) { logger.debug('Killing backend server process on Dredd\'s exit'); this.serverProcess.signalKill(); } }); const waitSecs = parseInt(this.argv['server-wait'], 10); const waitMilis = waitSecs * 1000; logger.debug(`Waiting ${waitSecs} seconds for backend server process to start`); this.wait = setTimeout(() => { this.runDredd(this.dreddInstance); }, waitMilis); } } // This should be handled in a better way in the future: // https://github.com/apiaryio/dredd/issues/625 logDebuggingInfo(config) { logger.debug('Dredd version:', packageData.version); logger.debug('Node.js version:', process.version); logger.debug('Node.js environment:', process.versions); logger.debug('System version:', os.type(), os.release(), os.arch()); try { const npmVersion = spawnSync('npm', ['--version']).stdout.toString().trim(); logger.debug('npm version:', npmVersion || 'unable to determine npm version'); } catch (err) { logger.debug('npm version: unable to determine npm version:', err); } logger.debug('Configuration:', JSON.stringify(config)); } run() { try { for (const task of [ this.setOptimistArgv, this.parseCustomConfig, this.runExitingActions, this.loadDreddFile, this.checkRequiredArgs, this.moveBlueprintArgToPath, ]) { task.call(this); if (this.finished) { return; } } const configurationForDredd = this.initConfig(); this.logDebuggingInfo(configurationForDredd); this.dreddInstance = this.initDredd(configurationForDredd); } catch (e) { this.exitWithStatus(e); } ignorePipeErrors(process); try { this.runServerAndThenDredd(); } catch (e) { logger.error(e.message, e.stack); this.stopServer(() => { this._processExit(2); }); } } lastArgvIsApiEndpoint() { // When API description path is a glob, some shells are automatically expanding globs and concating // result as arguments so I'm taking last argument as API endpoint server URL and removing it // from optimist's args this.server = this.argv._[this.argv._.length - 1]; this.argv._.splice(this.argv._.length - 1, 1); return this; } takeRestOfParamsAsPath() { // And rest of arguments concating to 'path' and 'p' opts, duplicates are filtered out later this.argv.p = this.argv.path = this.argv.path.concat(this.argv._); return this; } initConfig() { this.lastArgvIsApiEndpoint().takeRestOfParamsAsPath(); const configuration = { server: this.server, options: this.argv, }; // Push first argument (without some known configuration --key) into paths if (!configuration.options.path) { configuration.options.path = []; } configuration.options.path.push(this.argv._[0]); configuration.custom = this.custom; return configuration; } initDredd(configuration) { return new Dredd(configuration); } commandSigInt() { logger.error('\nShutting down from keyboard interruption (Ctrl+C)'); this.dreddInstance.transactionsComplete(() => this._processExit(0)); } runDredd(dreddInstance) { if (this.sigIntEventAdd) { // Handle SIGINT from user this.sigIntEventAdded = !(this.sigIntEventAdd = false); process.on('SIGINT', this.commandSigInt); } logger.debug('Running Dredd instance.'); dreddInstance.run((error, stats) => { logger.debug('Dredd instance run finished.'); this.exitWithStatus(error, stats); }); return this; } exitWithStatus(error, stats) { if (error) { if (error.message) { logger.error(error.message); } this._processExit(1); } if ((stats.failures + stats.errors) > 0) { this._processExit(1); } else { this._processExit(0); } } } module.exports = CLI;