Merge pull request #10 from node-ci/feature_expose_executor

Feature expose executor
This commit is contained in:
Oleg Korobenko 2016-03-16 21:38:53 +03:00
commit 84c6b8ee05
21 changed files with 459 additions and 221 deletions

12
app.js
View File

@ -16,6 +16,10 @@ var env = process.env.NODE_ENV || 'development',
ProjectsCollection = require('./lib/project').ProjectsCollection, ProjectsCollection = require('./lib/project').ProjectsCollection,
BuildsCollection = require('./lib/build').BuildsCollection, BuildsCollection = require('./lib/build').BuildsCollection,
libLogger = require('./lib/logger'), libLogger = require('./lib/logger'),
libNode = require('./lib/node'),
libCommand = require('./lib/command'),
libExecutor = require('./lib/executor'),
libScm = require('./lib/scm'),
EventEmitter = require('events').EventEmitter, EventEmitter = require('events').EventEmitter,
validateConfig = require('./lib/validateConfig'), validateConfig = require('./lib/validateConfig'),
utils = require('./lib/utils'); utils = require('./lib/utils');
@ -64,6 +68,10 @@ app.lib = {};
app.lib.BaseReaderLoader = BaseReaderLoader; app.lib.BaseReaderLoader = BaseReaderLoader;
app.lib.BaseNotifierTransport = BaseNotifierTransport; app.lib.BaseNotifierTransport = BaseNotifierTransport;
app.lib.logger = libLogger; app.lib.logger = libLogger;
app.lib.command = libCommand;
app.lib.executor = libExecutor;
app.lib.scm = libScm;
app.lib.node = libNode;
var configDefaults = { var configDefaults = {
notify: {}, notify: {},
@ -222,7 +230,7 @@ Steppy(
completeUncompletedBuilds(this.slot()); completeUncompletedBuilds(this.slot());
}, },
function() { function() {
require('./distributor').init(app, this.slot()); require('./distributor').create(app, this.slot());
}, },
function(err, distributor) { function(err, distributor) {
app.builds = new BuildsCollection({ app.builds = new BuildsCollection({
@ -236,6 +244,8 @@ Steppy(
require(plugin).register(app); require(plugin).register(app);
}); });
distributor.init();
app.notifier.init(app.config.notify, this.slot()); app.notifier.init(app.config.notify, this.slot());
}, },
function() { function() {

View File

@ -7,7 +7,7 @@ var Steppy = require('twostep').Steppy,
logger = require('./lib/logger')('distributor'); logger = require('./lib/logger')('distributor');
exports.init = function(app, callback) { exports.create = function(app, callback) {
var distributor = new Distributor({ var distributor = new Distributor({
nodes: app.config.nodes, nodes: app.config.nodes,
projects: app.projects, projects: app.projects,

View File

@ -4,23 +4,9 @@ var EventEmitter = require('events').EventEmitter,
inherits = require('util').inherits; inherits = require('util').inherits;
function Command(params) { function Command(params) {
params = params || {}; this.setParams(params);
this.emitIn = params.emitIn;
this.emitOut = params.emitOut;
this.emitErr = params.emitErr;
this.attachStderr = params.attachStderr;
} }
exports.Command = Command; exports.Command = Command;
inherits(Command, EventEmitter); inherits(Command, EventEmitter);
Command.prototype.enableEmitter = function() {
this.emitOut = true;
return this;
};
Command.prototype.disableEmitter = function() {
this.emitOut = false;
return this;
};

View File

@ -1,5 +1,9 @@
'use strict'; 'use strict';
var SpawnCommand = require('./spawn').Command;
exports.SpawnCommand = SpawnCommand;
exports.createCommand = function(params) { exports.createCommand = function(params) {
var Constructor = require('./' + params.type).Command; var Constructor = require('./' + params.type).Command;
return new Constructor(params); return new Constructor(params);

View File

@ -6,15 +6,22 @@ var spawn = require('child_process').spawn,
_ = require('underscore'); _ = require('underscore');
function Command(params) { function Command(params) {
params = params || {}; ParentCommand.call(this, params || {});
ParentCommand.call(this, params);
this.cwd = params.cwd;
} }
exports.Command = Command; exports.Command = Command;
inherits(Command, ParentCommand); 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` * Executes `params.cmd` with `params.args` and `params.options`
*/ */

View File

@ -2,42 +2,58 @@
var Steppy = require('twostep').Steppy, var Steppy = require('twostep').Steppy,
_ = require('underscore'), _ = require('underscore'),
Node = require('./node').Node, createNode = require('./node').createNode,
EventEmitter = require('events').EventEmitter, EventEmitter = require('events').EventEmitter,
inherits = require('util').inherits, inherits = require('util').inherits,
logger = require('./logger')('distributor'); logger = require('./logger')('distributor');
function Distributor(params) { function Distributor(params) {
var self = this; this.nodes = params.nodes;
// nodes to execute builds
self.nodes = _(params.nodes).map(function(nodeParams) {
return self._createNode(nodeParams);
});
// queued projects to build
self.queue = [];
self.saveBuild = params.saveBuild || function(build, callback) { // queued projects to build
this.queue = [];
this.saveBuild = params.saveBuild || function(build, callback) {
callback(null, build); callback(null, build);
}; };
self.removeBuild = params.removeBuild || function(build, callback) { this.removeBuild = params.removeBuild || function(build, callback) {
callback(); callback();
}; };
self.projects = params.projects; this.projects = params.projects;
self.notifier = params.notifier; this.notifier = params.notifier;
self.buildLogLineNumbersHash = {}, this.buildLogLineNumbersHash = {},
self.lastLinesHash = {}; this.lastLinesHash = {};
} }
inherits(Distributor, EventEmitter); inherits(Distributor, EventEmitter);
exports.Distributor = Distributor; exports.Distributor = Distributor;
Distributor.prototype._createNode = function(nodeParams) { // do deferred initialization (e.g. create nodes after all plugins load)
return new Node(nodeParams); 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) { Distributor.prototype._runNext = function(callback) {
@ -189,7 +205,7 @@ Distributor.prototype._updateWaitReasons = function() {
}); });
var waitReason = _(waitReasons).compact().join(', '); var waitReason = _(waitReasons).compact().join(', ');
// set only non-empty reasons // set only non-empty and new reasons
if (waitReason && waitReason !== item.build.waitReason) { if (waitReason && waitReason !== item.build.waitReason) {
self._updateBuild(item.build, {waitReason: waitReason}); self._updateBuild(item.build, {waitReason: waitReason});
} }

View File

@ -1,14 +1,12 @@
'use strict'; 'use strict';
var Steppy = require('twostep').Steppy, var Steppy = require('twostep').Steppy,
path = require('path'),
_ = require('underscore'), _ = require('underscore'),
EventEmitter = require('events').EventEmitter, EventEmitter = require('events').EventEmitter,
inherits = require('util').inherits; inherits = require('util').inherits;
function Executor(params) { function Executor(params) {
this.project = params.project; this.project = params.project;
this.cwd = path.join(this.project.dir, 'workspace');
} }
exports.Executor = Executor; exports.Executor = Executor;
@ -20,15 +18,135 @@ Executor.prototype.throttledEmit = _(function() {
}).throttle(500); }).throttle(500);
Executor.prototype._getSources = function(params, callback) { 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 // Does current project scm has new changes to build
Executor.prototype.hasScmChanges = function(callback) { 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) { Executor.prototype.run = function(params, callback) {

View File

@ -1,5 +1,9 @@
'use strict'; 'use strict';
var BaseExecutor = require('./base').Executor;
exports.BaseExecutor = BaseExecutor;
exports.createExecutor = function(params) { exports.createExecutor = function(params) {
var Constructor = require('./' + params.type).Executor; var Constructor = require('./' + params.type).Executor;
return new Constructor(params); return new Constructor(params);

View File

@ -6,10 +6,12 @@ var Steppy = require('twostep').Steppy,
createScm = require('../scm').createScm, createScm = require('../scm').createScm,
createCommand = require('../command').createCommand, createCommand = require('../command').createCommand,
fs = require('fs'), fs = require('fs'),
_ = require('underscore'); path = require('path'),
SpawnCommand = require('../command/spawn').Command;
function Executor(params) { function Executor(params) {
ParentExecutor.call(this, params); ParentExecutor.call(this, params);
this.cwd = path.join(this.project.dir, 'workspace');
} }
inherits(Executor, ParentExecutor); inherits(Executor, ParentExecutor);
@ -17,140 +19,17 @@ inherits(Executor, ParentExecutor);
exports.Executor = Executor; exports.Executor = Executor;
Executor.prototype._createScm = function(params) { Executor.prototype._createScm = function(params) {
params.command = new SpawnCommand();
return createScm(params); return createScm(params);
}; };
Executor.prototype._getChanges = function(params, callback) { Executor.prototype._createCommand = function(params) {
var self = this, return createCommand(params);
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.hasScmChanges = function(callback) { Executor.prototype._isCloned = function(callback) {
this._getChanges(this.project.scm, function(err, data) { fs.exists(this.cwd, function(exists) {
callback(err, !err && data.changes.length > 0); 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
);
};

View File

@ -1,17 +1,25 @@
'use strict'; 'use strict';
var _ = require('underscore'), var _ = require('underscore');
createExecutor = require('./executor').createExecutor;
function Node(params) { function Node(params) {
this.type = params.type; this.type = params.type;
this.maxExecutorsCount = params.maxExecutorsCount; 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 = {}; this.executors = {};
} }
exports.Node = Node; exports.Node = Node;
Node.prototype.usageStrategiesHash = {maximum: 1, specificProject: 1};
Node.prototype._getBlockerExecutor = function(getBlockers, getTarget) { Node.prototype._getBlockerExecutor = function(getBlockers, getTarget) {
return _(this.executors).find(function(executor) { return _(this.executors).find(function(executor) {
var target = getTarget(executor); var target = getTarget(executor);
@ -28,10 +36,20 @@ Node.prototype._getBlockerExecutor = function(getBlockers, getTarget) {
Node.prototype.getExecutorWaitReason = function(project) { Node.prototype.getExecutorWaitReason = function(project) {
var waitReason; var waitReason;
if (_(this.executors).size() >= this.maxExecutorsCount) { var targetNodeNames = project.node && project.node.target;
waitReason = 'All executors are busy';
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) { } else if (project.name in this.executors) {
waitReason = 'Project already running on node'; waitReason = this.name + ': project already running on node';
} else { } else {
var blockerExecutor; var blockerExecutor;
@ -59,7 +77,7 @@ Node.prototype.getExecutorWaitReason = function(project) {
if (blockerExecutor) { if (blockerExecutor) {
waitReason = ( waitReason = (
'Blocked by currently running "' + this.name + ': blocked by currently running "' +
blockerExecutor.project.name + '"' blockerExecutor.project.name + '"'
); );
} }
@ -76,13 +94,6 @@ Node.prototype.getFreeExecutorsCount = function() {
return this.maxExecutorsCount - _(this.executors).size(); return this.maxExecutorsCount - _(this.executors).size();
}; };
Node.prototype._createExecutor = function(project) {
return createExecutor({
type: this.type,
project: project
});
};
Node.prototype.hasScmChanges = function(project, callback) { Node.prototype.hasScmChanges = function(project, callback) {
this._createExecutor(project).hasScmChanges(callback); this._createExecutor(project).hasScmChanges(callback);
}; };

22
lib/node/index.js Normal file
View File

@ -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);
};

20
lib/node/local.js Normal file
View File

@ -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
});
};

View File

@ -1,22 +1,48 @@
'use strict'; 'use strict';
var ParentCommand = require('../command/spawn').Command, var EventEmitter = require('events').EventEmitter,
inherits = require('util').inherits; inherits = require('util').inherits,
_ = require('underscore');
function Scm(params) { function Scm(params) {
ParentCommand.call(this, params); var self = this;
this.repository = params.repository;
if (!this.repository && !this.cwd) throw new Error( EventEmitter.call(self);
'`repository` or `cwd` must be set'
); self.repository = params.repository;
this.collectOut = true; self.cwd = params.cwd;
this.emitIn = true;
this.attachStderr = true; 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; 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` * Clone repository to the `dst` update to `rev` and set `this.cwd` to `dst`

View File

@ -60,14 +60,14 @@ Scm.prototype.clone = function(dst, rev, callback) {
function() { function() {
// git can't clearly clone specified rev but can clone branch // git can't clearly clone specified rev but can clone branch
// possible solution to change clone params to (dst, branch, callback) // possible solution to change clone params to (dst, branch, callback)
self.run({ self._run({
cmd: 'git', cmd: 'git',
args: ['clone', '--recursive', self.repository, dst] args: ['clone', '--recursive', self.repository, dst]
}, this.slot()); }, this.slot());
self.cwd = dst; self.cwd = dst;
}, },
function() { function() {
self.run({cmd: 'git', args: ['checkout', '-f', rev]}, this.slot()); self._run({cmd: 'git', args: ['checkout', '-f', rev]}, this.slot());
}, },
callback callback
); );
@ -82,7 +82,7 @@ Scm.prototype.pull = function(rev, callback) {
}, },
function(err, currentRev) { function(err, currentRev) {
this.pass(currentRev); this.pass(currentRev);
self.run({cmd: 'git', args: ['pull']}, this.slot()); self._run({cmd: 'git', args: ['pull']}, this.slot());
}, },
function(err, currentRev) { function(err, currentRev) {
self.update(currentRev.id, this.slot()); self.update(currentRev.id, this.slot());
@ -104,7 +104,7 @@ Scm.prototype.getRev = function(rev, callback) {
var self = this; var self = this;
Steppy( Steppy(
function() { function() {
self.run({cmd: 'git', args: [ self._run({cmd: 'git', args: [
'show', rev, 'show', rev,
'--pretty=' + self._revTemplate '--pretty=' + self._revTemplate
]}, this.slot()); ]}, this.slot());
@ -146,7 +146,7 @@ Scm.prototype.getChanges = function(rev1, rev2, callback) {
function(err, currentRev) { function(err, currentRev) {
this.pass(currentRev); this.pass(currentRev);
self.run({cmd: 'git', args: [ self._run({cmd: 'git', args: [
'log', rev1 ? rev1 + '..' + rev2 : rev2, 'log', rev1 ? rev1 + '..' + rev2 : rev2,
'--pretty=' + self._revTemplate + self._linesSeparator '--pretty=' + self._revTemplate + self._linesSeparator
]}, this.slot()); ]}, this.slot());
@ -175,10 +175,10 @@ Scm.prototype.update = function(rev, callback) {
var self = this; var self = this;
Steppy( Steppy(
function() { function() {
self.run({cmd: 'git', args: ['checkout', '-f', rev]}, this.slot()); self._run({cmd: 'git', args: ['checkout', '-f', rev]}, this.slot());
}, },
function() { function() {
self.run({cmd: 'git', args: ['submodule', 'update']}, this.slot()); self._run({cmd: 'git', args: ['submodule', 'update']}, this.slot());
}, },
callback callback
); );

View File

@ -1,6 +1,23 @@
'use strict'; '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) { 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); return new Constructor(params);
}; };

View File

@ -55,7 +55,7 @@ Scm.prototype.clone = function(dst, rev, callback) {
var self = this; var self = this;
Steppy( Steppy(
function() { function() {
self.run({ self._run({
cmd: 'hg', cmd: 'hg',
args: ['clone', '--rev', rev, self.repository, dst] args: ['clone', '--rev', rev, self.repository, dst]
}, this.slot()); }, this.slot());
@ -66,14 +66,14 @@ Scm.prototype.clone = function(dst, rev, callback) {
}; };
Scm.prototype.pull = function(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) { Scm.prototype.getCurrent = function(callback) {
var self = this; var self = this;
Steppy( Steppy(
function() { function() {
self.run({cmd: 'hg', args: [ self._run({cmd: 'hg', args: [
'parent', '--template', self._revTemplate 'parent', '--template', self._revTemplate
]}, this.slot()); ]}, this.slot());
}, },
@ -88,7 +88,7 @@ Scm.prototype.getChanges = function(rev1, rev2, callback) {
var self = this; var self = this;
Steppy( Steppy(
function() { function() {
self.run({cmd: 'hg', args: [ self._run({cmd: 'hg', args: [
'log', '--rev', rev2 + ':' + rev1, 'log', '--rev', rev2 + ':' + rev1,
'--template', self._revTemplate + self._linesSeparator '--template', self._revTemplate + self._linesSeparator
]}, this.slot()); ]}, this.slot());
@ -111,5 +111,5 @@ Scm.prototype.getChanges = function(rev1, rev2, callback) {
}; };
Scm.prototype.update = function(rev, 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);
}; };

View File

@ -19,8 +19,11 @@ module.exports = function(config, callback) {
items: { items: {
type: 'object', type: 'object',
properties: { properties: {
type: {type: 'string', enum: ['local']}, name: {type: 'string'},
maxExecutorsCount: {type: 'integer'} type: {type: 'string'},
usageStrategy: {type: 'string'},
maxExecutorsCount: {type: 'integer'},
options: {type: 'object'}
} }
}, },
minItems: 1 minItems: 1

View File

@ -93,7 +93,7 @@ describe('Distributor blocking with max 2 executors count', function() {
var spy = updateBuildSpy; var spy = updateBuildSpy;
expect(spy.getCall(3).args[0].project.name).equal('project2'); expect(spy.getCall(3).args[0].project.name).equal('project2');
expect(spy.getCall(3).args[1].waitReason).equal( expect(spy.getCall(3).args[1].waitReason).equal(
'Blocked by currently running "project1"' 'local: blocked by currently running "project1"'
); );
}); });

View File

@ -2,16 +2,16 @@
var _ = require('underscore'), var _ = require('underscore'),
sinon = require('sinon'), sinon = require('sinon'),
Node = require('../../lib/node').Node, createNode = require('../../lib/node').createNode,
EventEmitter = require('events').EventEmitter, EventEmitter = require('events').EventEmitter,
ProjectsCollection = require('../../lib/project').ProjectsCollection, ProjectsCollection = require('../../lib/project').ProjectsCollection,
Distributor = require('../../lib/distributor').Distributor, Distributor = require('../../lib/distributor').Distributor,
Notifier = require('../../lib/notifier').Notifier; Notifier = require('../../lib/notifier').Notifier;
var createNode = function(executorRun) { var createMockedNode = function(executorRun) {
return function(params) { return function(params) {
var node = new Node(params); var node = createNode(params);
node._createExecutor = function(project) { node._createExecutor = function(project) {
var executor = new EventEmitter(); var executor = new EventEmitter();
executor.project = project; executor.project = project;
@ -38,7 +38,7 @@ exports.createDistributor = function(params) {
distributorParams.executorRun || sinon.stub().callsArgAsync(1) distributorParams.executorRun || sinon.stub().callsArgAsync(1)
); );
// patch method which will be called at constructor // patch method which will be called at constructor
sinon.stub(Distributor.prototype, '_createNode', createNode( sinon.stub(Distributor.prototype, '_createNode', createMockedNode(
executorRun executorRun
)); ));
delete distributorParams.executorRun; delete distributorParams.executorRun;
@ -52,6 +52,7 @@ exports.createDistributor = function(params) {
}); });
var distributor = new Distributor(distributorParams); var distributor = new Distributor(distributorParams);
distributor.init();
if (mockNode) { if (mockNode) {
Distributor.prototype._createNode.restore(); Distributor.prototype._createNode.restore();

View File

@ -1,7 +1,8 @@
'use strict'; 'use strict';
var Node = require('../lib/node').Node, var createNode = require('../lib/node').createNode,
expect = require('expect.js'); expect = require('expect.js'),
_ = require('underscore');
describe('Node', function() { describe('Node', function() {
@ -9,6 +10,114 @@ describe('Node', function() {
project1 = {name: 'project1'}, project1 = {name: 'project1'},
project2 = {name: 'project2'}; 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) { var expectNodeHasFreeExecutor = function(project, value) {
it('should' + (value ? ' ' : ' not ') + 'has free executors for ' + it('should' + (value ? ' ' : ' not ') + 'has free executors for ' +
project.name, function() { project.name, function() {
@ -19,7 +128,7 @@ describe('Node', function() {
describe('basic', function() { describe('basic', function() {
it('instance should be created without errors', function() { it('instance should be created without errors', function() {
node = new Node({ node = createNode({
type: 'local', type: 'local',
maxExecutorsCount: 1 maxExecutorsCount: 1
}); });

View File

@ -42,7 +42,8 @@ var expect = require('expect.js'),
it('create scm instance attached to new repository without errors', function() { it('create scm instance attached to new repository without errors', function() {
scm = createScm({ scm = createScm({
type: type, 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() { 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 ' + it('expect repository log from rev0 to default revision equals to ' +