From 96fa0920a4c0b20c5f767a6a8ece05a571d10499 Mon Sep 17 00:00:00 2001 From: Braydon Fuller Date: Tue, 1 Sep 2015 12:47:15 -0400 Subject: [PATCH] Add unit tests for start. --- lib/scaffold/start.js | 124 ++++++++++++++++++--------- test/scaffold/start.unit.js | 165 ++++++++++++++++++++++++++++++++++++ 2 files changed, 251 insertions(+), 38 deletions(-) create mode 100644 test/scaffold/start.unit.js diff --git a/lib/scaffold/start.js b/lib/scaffold/start.js index 704a9ff5..91488a8e 100644 --- a/lib/scaffold/start.js +++ b/lib/scaffold/start.js @@ -1,7 +1,6 @@ 'use strict'; var path = require('path'); -var socketio = require('socket.io'); var BitcoreNode = require('../node'); var index = require('../'); var bitcore = require('bitcore'); @@ -10,35 +9,43 @@ var $ = bitcore.util.preconditions; var log = index.log; log.debug = function() {}; -var count = 0; -var interval = false; - -function start(options) { - /* jshint maxstatements: 100 */ - +/** + * This function will loop over the configuration for services and require the + * specified modules, and assemble an array in this format: + * [ + * { + * name: 'bitcoind', + * config: {}, + * module: BitcoinService + * } + * ] + * @param {Function} req - The require function to use + * @param {Object} config + * @param {Array} config.services - An array of strings of service names. + * @returns {Array} + */ +function setupServices(req, config) { var services = []; - - var configPath = options.path; - var config = options.config; - if (config.services) { for (var i = 0; i < config.services.length; i++) { var service = {}; service.name = config.services[i]; - service.config = config.servicesConfig && config.servicesConfig[service.name] ? config.servicesConfig[service.name] : {}; + + var hasConfig = config.servicesConfig && config.servicesConfig[service.name]; + service.config = hasConfig ? config.servicesConfig[service.name] : {}; try { // first try in the built-in bitcore-node services directory - service.module = require(path.resolve(__dirname, '../services/' + service.name)); + service.module = req(path.resolve(__dirname, '../services/' + service.name)); } catch(e) { // check if the package.json specifies a specific file to use - var servicePackage = require(service.name + '/package.json'); + var servicePackage = req(service.name + '/package.json'); var serviceModule = service.name; if (servicePackage.bitcoreNode) { serviceModule = service.name + '/' + servicePackage.bitcoreNode; } - service.module = require(serviceModule); + service.module = req(serviceModule); } // check that the service supports expected methods @@ -54,16 +61,18 @@ function start(options) { services.push(service); } } + return services; +} - var fullConfig = _.clone(config); +/** + * Will register event handlers to log the current db sync status. + * @param {Node} node + */ +function registerSyncHandlers(node, delay) { - // expand to the full path - fullConfig.datadir = path.resolve(configPath, config.datadir); - - // load the services - fullConfig.services = services; - - var node = new BitcoreNode(fullConfig); + delay = delay || 10000; + var interval = false; + var count = 0; function logSyncStatus() { log.info( @@ -80,14 +89,6 @@ function start(options) { logSyncStatus(); }); - node.on('ready', function() { - log.info('Bitcore Node ready'); - }); - - node.on('error', function(err) { - log.error(err); - }); - node.on('ready', function() { node.services.db.on('addblock', function(block) { count++; @@ -96,7 +97,7 @@ function start(options) { interval = setInterval(function() { logSyncStatus(); count = 0; - }, 10000); + }, delay); } }); }); @@ -104,41 +105,88 @@ function start(options) { node.on('stopping', function() { clearInterval(interval); }); +} + +/** + * Will register event handlers to stop the node for `process` events + * `uncaughtException` and `SIGINT`. + * @param {Node} proc - The Node.js process + * @param {Node} node + */ +function registerExitHandlers(proc, node) { function exitHandler(options, err) { if (err) { log.error('uncaught exception:', err); if(err.stack) { - console.log(err.stack); + log.error(err.stack); } node.stop(function(err) { if(err) { log.error('Failed to stop services: ' + err); } - process.exit(-1); + proc.exit(-1); }); } if (options.sigint) { node.stop(function(err) { if(err) { log.error('Failed to stop services: ' + err); - return process.exit(1); + return proc.exit(1); } log.info('Halted'); - process.exit(0); + proc.exit(0); }); } } //catches uncaught exceptions - process.on('uncaughtException', exitHandler.bind(null, {exit:true})); + proc.on('uncaughtException', exitHandler.bind(null, {exit:true})); //catches ctrl+c event - process.on('SIGINT', exitHandler.bind(null, {sigint:true})); + proc.on('SIGINT', exitHandler.bind(null, {sigint:true})); +} + +/** + * This function will instantiate and start a Node, requiring the necessary service + * modules, and registering event handlers. + * @param {Object} options + * @param {String} options.path - The absolute path of the configuration file + * @param {Object} options.config - The parsed bitcore-node.json configuration file + * @param {Array} options.config.services - An array of services names. + * @param {Object} options.config.servicesConfig - Parameters to pass to each service + * @param {String} options.config.datadir - A relative (to options.path) or absolute path to the datadir + * @param {String} options.config.network - 'livenet', 'testnet' or 'regtest + * @param {Number} options.config.port - The port to use for the web service + */ +function start(options) { + + var fullConfig = _.clone(options.config); + fullConfig.services = setupServices(require, options.config); + fullConfig.datadir = path.resolve(options.path, options.config.datadir); + + var node = new BitcoreNode(fullConfig); + + // set up the event handlers for logging sync information + registerSyncHandlers(node); + + // setup handlers for uncaught exceptions and ctrl+c + registerExitHandlers(process, node); + + node.on('ready', function() { + log.info('Bitcore Node ready'); + }); + + node.on('error', function(err) { + log.error(err); + }); return node; } module.exports = start; +module.exports.registerExitHandlers = registerExitHandlers; +module.exports.registerSyncHandlers = registerSyncHandlers; +module.exports.setupServices = setupServices; diff --git a/test/scaffold/start.unit.js b/test/scaffold/start.unit.js new file mode 100644 index 00000000..14f9c5d7 --- /dev/null +++ b/test/scaffold/start.unit.js @@ -0,0 +1,165 @@ +'use strict'; + +var should = require('chai').should(); +var EventEmitter = require('events').EventEmitter; +var path = require('path'); +var sinon = require('sinon'); +var proxyquire = require('proxyquire'); +var start = require('../../lib/scaffold/start'); + +describe('#start', function() { + describe('#setupServices', function() { + var setupServices = proxyquire('../../lib/scaffold/start', {}).setupServices; + it('will require an internal module', function() { + function InternalService() {} + InternalService.dependencies = []; + InternalService.prototype.start = sinon.stub(); + InternalService.prototype.stop = sinon.stub(); + var expectedPath = path.resolve(__dirname, '../../lib/services/internal'); + var testRequire = function(p) { + p.should.equal(expectedPath); + return InternalService; + }; + var config = { + services: ['internal'], + servicesConfig: { + internal: { + param: 'value' + } + } + }; + var services = setupServices(testRequire, config); + services[0].name.should.equal('internal'); + services[0].config.should.deep.equal({param: 'value'}); + services[0].module.should.equal(InternalService); + }); + it('will require a local module', function() { + function LocalService() {} + LocalService.dependencies = []; + LocalService.prototype.start = sinon.stub(); + LocalService.prototype.stop = sinon.stub(); + var notfoundPath = path.resolve(__dirname, '../../lib/services/local'); + var testRequire = function(p) { + if (p === notfoundPath) { + throw new Error(); + } else if (p === 'local') { + return LocalService; + } else if (p === 'local/package.json') { + return { + name: 'local' + }; + } + }; + var config = { + services: ['local'] + }; + var services = setupServices(testRequire, config); + services[0].name.should.equal('local'); + services[0].module.should.equal(LocalService); + }); + it('will require a local module with "bitcoreNode" in package.json', function() { + function LocalService() {} + LocalService.dependencies = []; + LocalService.prototype.start = sinon.stub(); + LocalService.prototype.stop = sinon.stub(); + var notfoundPath = path.resolve(__dirname, '../../lib/services/local'); + var testRequire = function(p) { + if (p === notfoundPath) { + throw new Error(); + } else if (p === 'local/package.json') { + return { + name: 'local', + bitcoreNode: 'lib/bitcoreNode.js' + }; + } else if (p === 'local/lib/bitcoreNode.js') { + return LocalService; + } + }; + var config = { + services: ['local'] + }; + var services = setupServices(testRequire, config); + services[0].name.should.equal('local'); + services[0].module.should.equal(LocalService); + }); + it('will throw error if module is incompatible', function() { + var internal = {}; + var testRequire = function() { + return internal; + }; + var config = { + services: ['bitcoind'] + }; + (function() { + setupServices(testRequire, config); + }).should.throw('Could not load service'); + }); + }); + describe('#registerSyncHandlers', function() { + it('will log the sync status at an interval', function(done) { + var log = { + info: sinon.stub() + }; + var registerSyncHandlers = proxyquire('../../lib/scaffold/start', { + '../': { + log: log + } + }).registerSyncHandlers; + var node = new EventEmitter(); + node.services = { + db: new EventEmitter() + }; + node.services.db.tip = { + hash: 'hash', + __height: 10 + }; + registerSyncHandlers(node, 10); + node.emit('ready'); + node.services.db.emit('addblock'); + setTimeout(function() { + node.emit('synced'); + log.info.callCount.should.be.within(3, 4); + done(); + }, 35); + }); + }); + describe('#registerExitHandlers', function() { + var log = { + info: sinon.stub(), + error: sinon.stub() + }; + var registerExitHandlers = proxyquire('../../lib/scaffold/start', { + '../': { + log: log + } + }).registerExitHandlers; + it('log, stop and exit with an `uncaughtException`', function(done) { + var proc = new EventEmitter(); + proc.exit = sinon.stub(); + var node = { + stop: sinon.stub().callsArg(0) + }; + registerExitHandlers(proc, node); + proc.emit('uncaughtException', new Error('test')); + setImmediate(function() { + node.stop.callCount.should.equal(1); + proc.exit.callCount.should.equal(1); + done(); + }); + }); + it('stop and exit on `SIGINT`', function(done) { + var proc = new EventEmitter(); + proc.exit = sinon.stub(); + var node = { + stop: sinon.stub().callsArg(0) + }; + registerExitHandlers(proc, node); + proc.emit('SIGINT'); + setImmediate(function() { + node.stop.callCount.should.equal(1); + proc.exit.callCount.should.equal(1); + done(); + }); + }); + }); +});