diff --git a/app.js b/app.js index 3ac9439..250547b 100644 --- a/app.js +++ b/app.js @@ -16,6 +16,10 @@ var env = process.env.NODE_ENV || 'development', ProjectsCollection = require('./lib/project').ProjectsCollection, BuildsCollection = require('./lib/build').BuildsCollection, libLogger = require('./lib/logger'), + libNode = require('./lib/node'), + libCommand = require('./lib/command'), + libExecutor = require('./lib/executor'), + libScm = require('./lib/scm'), EventEmitter = require('events').EventEmitter, validateConfig = require('./lib/validateConfig'), utils = require('./lib/utils'); @@ -64,6 +68,10 @@ app.lib = {}; app.lib.BaseReaderLoader = BaseReaderLoader; app.lib.BaseNotifierTransport = BaseNotifierTransport; app.lib.logger = libLogger; +app.lib.command = libCommand; +app.lib.executor = libExecutor; +app.lib.scm = libScm; +app.lib.node = libNode; var configDefaults = { notify: {}, @@ -222,7 +230,7 @@ Steppy( completeUncompletedBuilds(this.slot()); }, function() { - require('./distributor').init(app, this.slot()); + require('./distributor').create(app, this.slot()); }, function(err, distributor) { app.builds = new BuildsCollection({ @@ -236,6 +244,8 @@ Steppy( require(plugin).register(app); }); + distributor.init(); + app.notifier.init(app.config.notify, this.slot()); }, function() { diff --git a/distributor.js b/distributor.js index e7e4a75..f523c68 100644 --- a/distributor.js +++ b/distributor.js @@ -7,7 +7,7 @@ var Steppy = require('twostep').Steppy, logger = require('./lib/logger')('distributor'); -exports.init = function(app, callback) { +exports.create = function(app, callback) { var distributor = new Distributor({ nodes: app.config.nodes, projects: app.projects, diff --git a/lib/command/base.js b/lib/command/base.js index 6b7b3e2..449940b 100644 --- a/lib/command/base.js +++ b/lib/command/base.js @@ -4,23 +4,9 @@ var EventEmitter = require('events').EventEmitter, inherits = require('util').inherits; function Command(params) { - params = params || {}; - this.emitIn = params.emitIn; - this.emitOut = params.emitOut; - this.emitErr = params.emitErr; - this.attachStderr = params.attachStderr; + this.setParams(params); } exports.Command = Command; inherits(Command, EventEmitter); - -Command.prototype.enableEmitter = function() { - this.emitOut = true; - return this; -}; - -Command.prototype.disableEmitter = function() { - this.emitOut = false; - return this; -}; diff --git a/lib/command/index.js b/lib/command/index.js index 4bf01c2..886f3ca 100644 --- a/lib/command/index.js +++ b/lib/command/index.js @@ -1,5 +1,9 @@ 'use strict'; +var SpawnCommand = require('./spawn').Command; + +exports.SpawnCommand = SpawnCommand; + exports.createCommand = function(params) { var Constructor = require('./' + params.type).Command; return new Constructor(params); diff --git a/lib/command/spawn.js b/lib/command/spawn.js index e136498..fddc534 100644 --- a/lib/command/spawn.js +++ b/lib/command/spawn.js @@ -6,15 +6,22 @@ var spawn = require('child_process').spawn, _ = require('underscore'); function Command(params) { - params = params || {}; - ParentCommand.call(this, params); - this.cwd = params.cwd; + ParentCommand.call(this, params || {}); } exports.Command = Command; inherits(Command, ParentCommand); +Command.prototype.setParams = function(params) { + if (params.cwd) this.cwd = params.cwd; + if (params.emitIn) this.emitIn = params.emitIn; + if (params.emitOut) this.emitOut = params.emitOut; + if (params.emitErr) this.emitErr = params.emitErr; + if (params.attachStderr) this.attachStderr = params.attachStderr; + if (params.collectOut) this.collectOut = params.collectOut; +}; + /** * Executes `params.cmd` with `params.args` and `params.options` */ diff --git a/lib/distributor.js b/lib/distributor.js index 8f70277..df5d8ec 100644 --- a/lib/distributor.js +++ b/lib/distributor.js @@ -2,42 +2,58 @@ var Steppy = require('twostep').Steppy, _ = require('underscore'), - Node = require('./node').Node, + createNode = require('./node').createNode, EventEmitter = require('events').EventEmitter, inherits = require('util').inherits, logger = require('./logger')('distributor'); function Distributor(params) { - var self = this; - // nodes to execute builds - self.nodes = _(params.nodes).map(function(nodeParams) { - return self._createNode(nodeParams); - }); - // queued projects to build - self.queue = []; + this.nodes = params.nodes; - self.saveBuild = params.saveBuild || function(build, callback) { + // queued projects to build + this.queue = []; + + this.saveBuild = params.saveBuild || function(build, callback) { callback(null, build); }; - self.removeBuild = params.removeBuild || function(build, callback) { + this.removeBuild = params.removeBuild || function(build, callback) { callback(); }; - self.projects = params.projects; - self.notifier = params.notifier; + this.projects = params.projects; + this.notifier = params.notifier; - self.buildLogLineNumbersHash = {}, - self.lastLinesHash = {}; + this.buildLogLineNumbersHash = {}, + this.lastLinesHash = {}; } inherits(Distributor, EventEmitter); exports.Distributor = Distributor; -Distributor.prototype._createNode = function(nodeParams) { - return new Node(nodeParams); +// do deferred initialization (e.g. create nodes after all plugins load) +Distributor.prototype.init = function() { + var self = this, + namesHash = {}; + + self.nodes = _(self.nodes).map(function(nodeParams) { + if (!nodeParams.name) { + nodeParams.name = nodeParams.type; + } + + if (namesHash[nodeParams.name]) { + throw new Error('Node name `' + nodeParams.name + '` already used'); + } + namesHash[nodeParams.name] = 1; + + return self._createNode(nodeParams); + }); +}; + +Distributor.prototype._createNode = function(params) { + return createNode(params); }; Distributor.prototype._runNext = function(callback) { @@ -189,7 +205,7 @@ Distributor.prototype._updateWaitReasons = function() { }); var waitReason = _(waitReasons).compact().join(', '); - // set only non-empty reasons + // set only non-empty and new reasons if (waitReason && waitReason !== item.build.waitReason) { self._updateBuild(item.build, {waitReason: waitReason}); } diff --git a/lib/executor/base.js b/lib/executor/base.js index 7411a1f..f5144ae 100644 --- a/lib/executor/base.js +++ b/lib/executor/base.js @@ -1,14 +1,12 @@ 'use strict'; var Steppy = require('twostep').Steppy, - path = require('path'), _ = require('underscore'), EventEmitter = require('events').EventEmitter, inherits = require('util').inherits; function Executor(params) { this.project = params.project; - this.cwd = path.join(this.project.dir, 'workspace'); } exports.Executor = Executor; @@ -20,15 +18,135 @@ Executor.prototype.throttledEmit = _(function() { }).throttle(500); Executor.prototype._getSources = function(params, callback) { + var self = this, + scm; + Steppy( + function() { + self._getChanges(params, this.slot()); + }, + function(err, data) { + scm = data.scm; + this.pass(data.changes); + scm.update(data.rev, this.slot()); + }, + function(err, changes) { + scm.getCurrent(this.slot()); + this.pass(changes); + scm.getRev(params.rev, this.slot()); + }, + function(err, currentRev, changes, latestRev) { + this.pass({ + rev: currentRev, + changes: changes, + isLatest: currentRev.id === latestRev.id + }); + }, + callback + ); }; -Executor.prototype._runStep = function(params, callback) { +Executor.prototype._runStep = function(step, callback) { + var self = this, + params = _(step).clone(); + Steppy( + function() { + if (params.type !== 'shell') { + throw new Error('Unknown step type: ' + params.type); + } + // set command cwd to executor cwd + params.cwd = self.cwd; + var command = self._createCommand( + _({ + emitIn: true, + emitOut: true, + emitErr: true, + attachStderr: true + }).extend(params) + ); + + command.on('stdin', function(data) { + self.emit('data', '> ' + String(data)); + }); + + command.on('stdout', function(data) { + self.emit('data', String(data)); + }); + + command.on('stderr', function(data) { + self.emit('data', 'stderr: ' + String(data)); + }); + + // TODO: should be fixed properly, currently it's quick fix for + // NODE_ENV which affects npm install/prune calls + params.options = params.options || {}; + params.options.env = params.options.env || process.env; + delete params.options.env.NODE_ENV; + + command.run(params, this.slot()) + }, + callback + ); +}; + +Executor.prototype._getChanges = function(params, callback) { + var self = this, + scm, isFirstRun, oldRev; + Steppy( + function() { + self._isCloned(this.slot()); + }, + function(err, cloned) { + var scmParams = {type: params.type}; + if (cloned) { + scmParams.cwd = self.cwd; + isFirstRun = false; + } else { + scmParams.repository = params.repository; + isFirstRun = true; + } + scm = self._createScm(scmParams); + + scm.on('stdin', function(data) { + self.emit('data', '> ' + String(data)); + }); + + if (isFirstRun) { + this.pass(null); + } else { + scm.getCurrent(this.slot()); + } + }, + function(err, id) { + oldRev = id; + + if (isFirstRun) { + scm.clone(self.cwd, params.rev, this.slot()); + } else { + scm.pull(params.rev, this.slot()) + } + }, + function() { + scm.getChanges(oldRev && oldRev.id, params.rev, this.slot()); + }, + function(err, changes) { + var target = self._getTarget(params.rev, changes); + this.pass({ + scm: scm, + oldRev: oldRev, + rev: target.rev, + changes: target.changes + }); + }, + callback + ); }; // Does current project scm has new changes to build Executor.prototype.hasScmChanges = function(callback) { - + this._getChanges(this.project.scm, function(err, data) { + callback(err, !err && data.changes.length > 0); + }); }; Executor.prototype.run = function(params, callback) { diff --git a/lib/executor/index.js b/lib/executor/index.js index c17c42d..ba8c43b 100644 --- a/lib/executor/index.js +++ b/lib/executor/index.js @@ -1,5 +1,9 @@ 'use strict'; +var BaseExecutor = require('./base').Executor; + +exports.BaseExecutor = BaseExecutor; + exports.createExecutor = function(params) { var Constructor = require('./' + params.type).Executor; return new Constructor(params); diff --git a/lib/executor/local.js b/lib/executor/local.js index 6043cd1..d701531 100644 --- a/lib/executor/local.js +++ b/lib/executor/local.js @@ -6,10 +6,12 @@ var Steppy = require('twostep').Steppy, createScm = require('../scm').createScm, createCommand = require('../command').createCommand, fs = require('fs'), - _ = require('underscore'); + path = require('path'), + SpawnCommand = require('../command/spawn').Command; function Executor(params) { ParentExecutor.call(this, params); + this.cwd = path.join(this.project.dir, 'workspace'); } inherits(Executor, ParentExecutor); @@ -17,140 +19,17 @@ inherits(Executor, ParentExecutor); exports.Executor = Executor; Executor.prototype._createScm = function(params) { + params.command = new SpawnCommand(); + return createScm(params); }; -Executor.prototype._getChanges = function(params, callback) { - var self = this, - scm, isFirstRun, oldRev; - Steppy( - function() { - var slot = this.slot(); - fs.exists(self.cwd, function(exists) { - slot(null, exists); - }); - }, - function(err, exists) { - var scmParams = {type: params.type}; - if (exists) { - scmParams.cwd = self.cwd; - isFirstRun = false; - } else { - scmParams.repository = params.repository; - isFirstRun = true; - } - scm = self._createScm(scmParams); - - scm.on('stdin', function(data) { - self.emit('data', '> ' + String(data)); - }); - - if (isFirstRun) { - this.pass(null); - } else { - scm.getCurrent(this.slot()); - } - }, - function(err, id) { - oldRev = id; - - if (isFirstRun) { - scm.clone(self.cwd, params.rev, this.slot()); - } else { - scm.pull(params.rev, this.slot()) - } - }, - function() { - scm.getChanges(oldRev && oldRev.id, params.rev, this.slot()); - }, - function(err, changes) { - var target = self._getTarget(params.rev, changes); - this.pass({ - scm: scm, - oldRev: oldRev, - rev: target.rev, - changes: target.changes - }); - }, - callback - ); +Executor.prototype._createCommand = function(params) { + return createCommand(params); }; -Executor.prototype.hasScmChanges = function(callback) { - this._getChanges(this.project.scm, function(err, data) { - callback(err, !err && data.changes.length > 0); +Executor.prototype._isCloned = function(callback) { + fs.exists(this.cwd, function(exists) { + callback(null, exists); }); }; - -Executor.prototype._getSources = function(params, callback) { - var self = this, - scm; - Steppy( - function() { - self._getChanges(params, this.slot()); - }, - function(err, data) { - scm = data.scm; - this.pass(data.changes); - scm.update(data.rev, this.slot()); - }, - function(err, changes) { - scm.getCurrent(this.slot()); - this.pass(changes); - scm.getRev(params.rev, this.slot()); - }, - function(err, currentRev, changes, latestRev) { - this.pass({ - rev: currentRev, - changes: changes, - isLatest: currentRev.id === latestRev.id - }); - }, - callback - ); -}; - -Executor.prototype._runStep = function(step, callback) { - var self = this, - params = _(step).clone(); - - Steppy( - function() { - if (params.type !== 'shell') { - throw new Error('Unknown step type: ' + params.type); - } - // set command cwd to executor cwd - params.cwd = self.cwd; - var command = createCommand( - _({ - emitIn: true, - emitOut: true, - emitErr: true, - attachStderr: true - }).extend(params) - ); - - command.on('stdin', function(data) { - self.emit('data', '> ' + String(data)); - }); - - command.on('stdout', function(data) { - self.emit('data', String(data)); - }); - - command.on('stderr', function(data) { - self.emit('data', 'stderr: ' + String(data)); - }); - - // TODO: should be fixed properly, currently it's quick fix for - // NODE_ENV which affects npm install/prune calls - params.options = params.options || {}; - params.options.env = params.options.env || process.env; - delete params.options.env.NODE_ENV; - - command.run(params, this.slot()) - }, - callback - ); -}; - diff --git a/lib/node.js b/lib/node/base.js similarity index 67% rename from lib/node.js rename to lib/node/base.js index 0238375..5843a65 100644 --- a/lib/node.js +++ b/lib/node/base.js @@ -1,17 +1,25 @@ 'use strict'; -var _ = require('underscore'), - createExecutor = require('./executor').createExecutor; +var _ = require('underscore'); function Node(params) { this.type = params.type; this.maxExecutorsCount = params.maxExecutorsCount; + this.name = params.name; + this.usageStrategy = params.usageStrategy || 'maximum'; + + if (!this.usageStrategiesHash[this.usageStrategy]) { + throw new Error('Unknown usage strategy: ' + this.usageStrategy); + } + this.executors = {}; } exports.Node = Node; +Node.prototype.usageStrategiesHash = {maximum: 1, specificProject: 1}; + Node.prototype._getBlockerExecutor = function(getBlockers, getTarget) { return _(this.executors).find(function(executor) { var target = getTarget(executor); @@ -28,10 +36,20 @@ Node.prototype._getBlockerExecutor = function(getBlockers, getTarget) { Node.prototype.getExecutorWaitReason = function(project) { var waitReason; - if (_(this.executors).size() >= this.maxExecutorsCount) { - waitReason = 'All executors are busy'; + var targetNodeNames = project.node && project.node.target; + + if (targetNodeNames && !_(targetNodeNames).isArray()) { + targetNodeNames = [targetNodeNames]; + } + + if (targetNodeNames && !_(targetNodeNames).contains(this.name)) { + waitReason = this.name + ': not a target node'; + } else if (this.usageStrategy === 'specificProject' && !targetNodeNames) { + waitReason = this.name + ': only for specific projects'; + } else if (_(this.executors).size() >= this.maxExecutorsCount) { + waitReason = this.name + ': all executors are busy'; } else if (project.name in this.executors) { - waitReason = 'Project already running on node'; + waitReason = this.name + ': project already running on node'; } else { var blockerExecutor; @@ -59,7 +77,7 @@ Node.prototype.getExecutorWaitReason = function(project) { if (blockerExecutor) { waitReason = ( - 'Blocked by currently running "' + + this.name + ': blocked by currently running "' + blockerExecutor.project.name + '"' ); } @@ -76,13 +94,6 @@ Node.prototype.getFreeExecutorsCount = function() { return this.maxExecutorsCount - _(this.executors).size(); }; -Node.prototype._createExecutor = function(project) { - return createExecutor({ - type: this.type, - project: project - }); -}; - Node.prototype.hasScmChanges = function(project, callback) { this._createExecutor(project).hasScmChanges(callback); }; diff --git a/lib/node/index.js b/lib/node/index.js new file mode 100644 index 0000000..d62fd7e --- /dev/null +++ b/lib/node/index.js @@ -0,0 +1,22 @@ +'use strict'; + +var BaseNode = require('./base').Node; + +exports.BaseNode = BaseNode; + +var constructors = { + local: require('./local').Node +}; + +exports.register = function(type, constructor) { + constructors[type] = constructor; +}; + +exports.createNode = function(params) { + if (params.type in constructors === false) { + throw new Error('Unknown node type: ' + params.type); + } + + var Constructor = constructors[params.type]; + return new Constructor(params); +}; diff --git a/lib/node/local.js b/lib/node/local.js new file mode 100644 index 0000000..efe9fbd --- /dev/null +++ b/lib/node/local.js @@ -0,0 +1,20 @@ +'user strict'; + +var inherits = require('util').inherits, + ParentNode = require('./base').Node, + LocalExecutor = require('../executor/local').Executor; + +function Node(params) { + ParentNode.call(this, params); +} + +inherits(Node, ParentNode); + +exports.Node = Node; + +Node.prototype._createExecutor = function(project) { + return new LocalExecutor({ + type: this.type, + project: project + }); +}; diff --git a/lib/scm/base.js b/lib/scm/base.js index 222182a..a9ab156 100644 --- a/lib/scm/base.js +++ b/lib/scm/base.js @@ -1,22 +1,48 @@ 'use strict'; -var ParentCommand = require('../command/spawn').Command, - inherits = require('util').inherits; +var EventEmitter = require('events').EventEmitter, + inherits = require('util').inherits, + _ = require('underscore'); function Scm(params) { - ParentCommand.call(this, params); - this.repository = params.repository; - if (!this.repository && !this.cwd) throw new Error( - '`repository` or `cwd` must be set' - ); - this.collectOut = true; - this.emitIn = true; - this.attachStderr = true; + var self = this; + + EventEmitter.call(self); + + self.repository = params.repository; + self.cwd = params.cwd; + + if (!self.repository && !self.cwd) { + throw new Error('`repository` or `cwd` must be set'); + } + + self.command = params.command; + + if (!self.command) throw new Error('`command` is required'); + + self.command.setParams({ + collectOut: true, + emitIn: true, + attachStderr: true + }); + + self.command.on('stdin', function(data) { + self.emit('stdin', data); + }); } exports.Scm = Scm; -inherits(Scm, ParentCommand); +inherits(Scm, EventEmitter); + +Scm.prototype._run = function(params, callback) { + if (this.cwd) { + params.options = params.options || {}; + params.options.cwd = this.cwd; + } + + this.command.run(params, callback); +}; /** * Clone repository to the `dst` update to `rev` and set `this.cwd` to `dst` diff --git a/lib/scm/git.js b/lib/scm/git.js index f9fcf62..49c3f5c 100644 --- a/lib/scm/git.js +++ b/lib/scm/git.js @@ -60,14 +60,14 @@ Scm.prototype.clone = function(dst, rev, callback) { function() { // git can't clearly clone specified rev but can clone branch // possible solution to change clone params to (dst, branch, callback) - self.run({ + self._run({ cmd: 'git', args: ['clone', '--recursive', self.repository, dst] }, this.slot()); self.cwd = dst; }, function() { - self.run({cmd: 'git', args: ['checkout', '-f', rev]}, this.slot()); + self._run({cmd: 'git', args: ['checkout', '-f', rev]}, this.slot()); }, callback ); @@ -82,7 +82,7 @@ Scm.prototype.pull = function(rev, callback) { }, function(err, currentRev) { this.pass(currentRev); - self.run({cmd: 'git', args: ['pull']}, this.slot()); + self._run({cmd: 'git', args: ['pull']}, this.slot()); }, function(err, currentRev) { self.update(currentRev.id, this.slot()); @@ -104,7 +104,7 @@ Scm.prototype.getRev = function(rev, callback) { var self = this; Steppy( function() { - self.run({cmd: 'git', args: [ + self._run({cmd: 'git', args: [ 'show', rev, '--pretty=' + self._revTemplate ]}, this.slot()); @@ -146,7 +146,7 @@ Scm.prototype.getChanges = function(rev1, rev2, callback) { function(err, currentRev) { this.pass(currentRev); - self.run({cmd: 'git', args: [ + self._run({cmd: 'git', args: [ 'log', rev1 ? rev1 + '..' + rev2 : rev2, '--pretty=' + self._revTemplate + self._linesSeparator ]}, this.slot()); @@ -175,10 +175,10 @@ Scm.prototype.update = function(rev, callback) { var self = this; Steppy( function() { - self.run({cmd: 'git', args: ['checkout', '-f', rev]}, this.slot()); + self._run({cmd: 'git', args: ['checkout', '-f', rev]}, this.slot()); }, function() { - self.run({cmd: 'git', args: ['submodule', 'update']}, this.slot()); + self._run({cmd: 'git', args: ['submodule', 'update']}, this.slot()); }, callback ); diff --git a/lib/scm/index.js b/lib/scm/index.js index 89d0ddc..f68bb6e 100644 --- a/lib/scm/index.js +++ b/lib/scm/index.js @@ -1,6 +1,23 @@ 'use strict'; +var BaseScm = require('./base').Scm; + +exports.BaseScm = BaseScm; + +var constructors = { + git: require('./git').Scm, + mercurial: require('./mercurial').Scm +}; + +exports.register = function(type, constructor) { + constructors[type] = constructor; +}; + exports.createScm = function(params) { - var Constructor = require('./' + params.type).Scm; + if (params.type in constructors === false) { + throw new Error('Unknown scm type: ' + params.type); + } + + var Constructor = constructors[params.type]; return new Constructor(params); }; diff --git a/lib/scm/mercurial.js b/lib/scm/mercurial.js index c69a4e2..01c6b7e 100644 --- a/lib/scm/mercurial.js +++ b/lib/scm/mercurial.js @@ -55,7 +55,7 @@ Scm.prototype.clone = function(dst, rev, callback) { var self = this; Steppy( function() { - self.run({ + self._run({ cmd: 'hg', args: ['clone', '--rev', rev, self.repository, dst] }, this.slot()); @@ -66,14 +66,14 @@ Scm.prototype.clone = function(dst, rev, callback) { }; Scm.prototype.pull = function(rev, callback) { - this.run({cmd: 'hg', args: ['pull', '--rev', rev]}, callback); + this._run({cmd: 'hg', args: ['pull', '--rev', rev]}, callback); }; Scm.prototype.getCurrent = function(callback) { var self = this; Steppy( function() { - self.run({cmd: 'hg', args: [ + self._run({cmd: 'hg', args: [ 'parent', '--template', self._revTemplate ]}, this.slot()); }, @@ -88,7 +88,7 @@ Scm.prototype.getChanges = function(rev1, rev2, callback) { var self = this; Steppy( function() { - self.run({cmd: 'hg', args: [ + self._run({cmd: 'hg', args: [ 'log', '--rev', rev2 + ':' + rev1, '--template', self._revTemplate + self._linesSeparator ]}, this.slot()); @@ -111,5 +111,5 @@ Scm.prototype.getChanges = function(rev1, rev2, callback) { }; Scm.prototype.update = function(rev, callback) { - this.run({cmd: 'hg', args: ['up', '-C', rev]}, callback); + this._run({cmd: 'hg', args: ['up', '-C', rev]}, callback); }; diff --git a/lib/validateConfig.js b/lib/validateConfig.js index e144773..89c007c 100644 --- a/lib/validateConfig.js +++ b/lib/validateConfig.js @@ -19,8 +19,11 @@ module.exports = function(config, callback) { items: { type: 'object', properties: { - type: {type: 'string', enum: ['local']}, - maxExecutorsCount: {type: 'integer'} + name: {type: 'string'}, + type: {type: 'string'}, + usageStrategy: {type: 'string'}, + maxExecutorsCount: {type: 'integer'}, + options: {type: 'object'} } }, minItems: 1 diff --git a/test/distributor/blocking.js b/test/distributor/blocking.js index 3b009f0..e199fd1 100644 --- a/test/distributor/blocking.js +++ b/test/distributor/blocking.js @@ -93,7 +93,7 @@ describe('Distributor blocking with max 2 executors count', function() { var spy = updateBuildSpy; expect(spy.getCall(3).args[0].project.name).equal('project2'); expect(spy.getCall(3).args[1].waitReason).equal( - 'Blocked by currently running "project1"' + 'local: blocked by currently running "project1"' ); }); diff --git a/test/distributor/helpers.js b/test/distributor/helpers.js index 2da42c6..a66da3b 100644 --- a/test/distributor/helpers.js +++ b/test/distributor/helpers.js @@ -2,16 +2,16 @@ var _ = require('underscore'), sinon = require('sinon'), - Node = require('../../lib/node').Node, + createNode = require('../../lib/node').createNode, EventEmitter = require('events').EventEmitter, ProjectsCollection = require('../../lib/project').ProjectsCollection, Distributor = require('../../lib/distributor').Distributor, Notifier = require('../../lib/notifier').Notifier; -var createNode = function(executorRun) { +var createMockedNode = function(executorRun) { return function(params) { - var node = new Node(params); + var node = createNode(params); node._createExecutor = function(project) { var executor = new EventEmitter(); executor.project = project; @@ -38,7 +38,7 @@ exports.createDistributor = function(params) { distributorParams.executorRun || sinon.stub().callsArgAsync(1) ); // patch method which will be called at constructor - sinon.stub(Distributor.prototype, '_createNode', createNode( + sinon.stub(Distributor.prototype, '_createNode', createMockedNode( executorRun )); delete distributorParams.executorRun; @@ -52,6 +52,7 @@ exports.createDistributor = function(params) { }); var distributor = new Distributor(distributorParams); + distributor.init(); if (mockNode) { Distributor.prototype._createNode.restore(); diff --git a/test/node.js b/test/node.js index add084d..f932969 100644 --- a/test/node.js +++ b/test/node.js @@ -1,7 +1,8 @@ 'use strict'; -var Node = require('../lib/node').Node, - expect = require('expect.js'); +var createNode = require('../lib/node').createNode, + expect = require('expect.js'), + _ = require('underscore'); describe('Node', function() { @@ -9,6 +10,114 @@ describe('Node', function() { project1 = {name: 'project1'}, project2 = {name: 'project2'}; + var createNodeMock = function(params) { + params = params || {}; + + var node = createNode(_({ + name: 'executor1', + type: 'local', + maxExecutorsCount: 1, + usageStrategy: 'maximum' + }).extend(params)); + + // only for testing + if (params.executors) { + node.executors = params.executors; + } + return node; + }; + + describe('wait reason', function() { + + it('should be not a target node when node target is not match', function() { + var waitReason = createNodeMock({ + name: 'executor1' + }).getExecutorWaitReason({ + name: 'project1', + node: {target: 'other executor'} + }); + expect(waitReason).eql('executor1: not a target node'); + }); + + it('should be falsy when node target match', function() { + var waitReason = createNodeMock({ + name: 'executor1' + }).getExecutorWaitReason({ + name: 'project1', + node: {target: 'executor1'} + }); + expect(waitReason).not.ok(); + }); + + it('should be falsy when node target (array) match', function() { + var waitReason = createNodeMock({ + name: 'executor1' + }).getExecutorWaitReason({ + name: 'project1', + node: {target: ['executor1']} + }); + expect(waitReason).not.ok(); + }); + + it('should be only for specific projects when target is not set', function() { + var waitReason = createNodeMock({ + usageStrategy: 'specificProject' + }).getExecutorWaitReason({ + name: 'project1' + }); + expect(waitReason).eql('executor1: only for specific projects'); + }); + + it('should be all executors are busy when true', function() { + var waitReason = createNodeMock({ + maxExecutorsCount: 1, + executors: {project2: 1} + }).getExecutorWaitReason({ + name: 'project1' + }); + expect(waitReason).eql('executor1: all executors are busy'); + }); + + it('should be project already running on node when true', function() { + var waitReason = createNodeMock({ + maxExecutorsCount: 2, + executors: {project1: 1} + }).getExecutorWaitReason({ + name: 'project1' + }); + expect(waitReason).eql('executor1: project already running on node'); + }); + + it('should be blocked by project when blocked by executing', function() { + var waitReason = createNodeMock({ + maxExecutorsCount: 2, + executors: {project2: {project: {name: 'project2'}}} + }).getExecutorWaitReason({ + name: 'project1', + blockedBy: ['project2'] + }); + expect(waitReason).eql( + 'executor1: blocked by currently running "project2"' + ); + }); + + it('should be blocked by project when executing blocks it', function() { + var waitReason = createNodeMock({ + maxExecutorsCount: 2, + executors: {project2: {project: { + name: 'project2', + blocks: ['project1'] + }}} + }).getExecutorWaitReason({ + name: 'project1' + }); + expect(waitReason).eql( + 'executor1: blocked by currently running "project2"' + ); + }); + + }); + var expectNodeHasFreeExecutor = function(project, value) { it('should' + (value ? ' ' : ' not ') + 'has free executors for ' + project.name, function() { @@ -19,7 +128,7 @@ describe('Node', function() { describe('basic', function() { it('instance should be created without errors', function() { - node = new Node({ + node = createNode({ type: 'local', maxExecutorsCount: 1 }); diff --git a/test/scm.js b/test/scm.js index 214da67..3e9717a 100644 --- a/test/scm.js +++ b/test/scm.js @@ -42,7 +42,8 @@ var expect = require('expect.js'), it('create scm instance attached to new repository without errors', function() { scm = createScm({ type: type, - repository: originalRepositoryPath + repository: originalRepositoryPath, + command: new SpawnCommand() }); }); @@ -129,7 +130,11 @@ var expect = require('expect.js'), }); it('create scm instance attached to existing `cwd` without errors', function() { - scm = createScm({type: type, cwd: repositoryPath}); + scm = createScm({ + type: type, + cwd: repositoryPath, + command: new SpawnCommand() + }); }); it('expect repository log from rev0 to default revision equals to ' +