Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RunContext enhancements #570

Merged
merged 3 commits into from
Jun 5, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 19 additions & 7 deletions lib/test/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -244,13 +244,8 @@ exports.createDummyGenerator = function () {

exports.createGenerator = function (name, dependencies, args, options) {
var env = generators();
dependencies.forEach(function (d) {
if (d instanceof Array) {
env.registerStub(d[0], d[1]);
} else {
env.register(d);
}
});

this.registerDependencies(env, dependencies);

var generator = env.create(name, { arguments: args, options: options });

Expand All @@ -267,6 +262,23 @@ exports.createGenerator = function (name, dependencies, args, options) {
return generator;
};

/**
* Register a list of dependent generators into the provided env.
* Dependecies can be path (autodiscovery) or an array [<generator>, <name>]
*
* @param {Array} dependencies - paths to the generators dependencies
*/

exports.registerDependencies = function (env, dependencies) {
dependencies.forEach(function (d) {
if (_.isArray(d)) {
env.registerStub(d[0], d[1]);
} else {
env.register(d);
}
});
};

/**
* Run the provided Generator
* @param {String|Function} Generator - Generator constructor or namespace
Expand Down
90 changes: 63 additions & 27 deletions lib/test/run-context.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
'use strict';
var path = require('path');
var assert = require('assert');
var _ = require('lodash');
var yeoman = require('../..');
var util = require('util');
Expand All @@ -16,43 +18,28 @@ var helpers = require('./helpers');
*/

var RunContext = module.exports = function RunContext(Generator) {
var namespace;

this._asyncHolds = 0;
this.runned = false;
this.env = yeoman();
this.args = [];
this.options = {};
this._dependencies = [];
this.Generator = Generator;

if (_.isString(Generator)) {
namespace = this.env.namespace(Generator);
this.env.register(Generator);
} else {
namespace = 'gen:test';
this.env.registerStub(Generator, namespace);
}

this.generator = this.env.create(namespace);

// Mock prompt by default
this.withPrompt();

setTimeout(this._onReady.bind(this), 0);
setTimeout(this._run.bind(this), 0);
};

util.inherits(RunContext, EventEmitter);

/**
* Hold the execution until the returned callback is triggered
* @private
* @return {Function} Callback to notify the normal execution can resume
*/

RunContext.prototype._holdExec = function () {
RunContext.prototype.async = function () {
this._asyncHolds++;
return function () {
this._asyncHolds--;
this._onReady();
this._run();
}.bind(this);
};

Expand All @@ -61,14 +48,34 @@ RunContext.prototype._holdExec = function () {
* @private
*/

RunContext.prototype._onReady = function () {
RunContext.prototype._run = function () {
if (this._asyncHolds !== 0 || this.runned) return;
this.runned = true;

var namespace;
this.env = yeoman();

helpers.registerDependencies(this.env, this._dependencies);

if (_.isString(this.Generator)) {
namespace = this.env.namespace(this.Generator);
this.env.register(this.Generator);
} else {
namespace = 'gen:test';
this.env.registerStub(this.Generator, namespace);
}

this.generator = this.env.create(namespace);
helpers.mockPrompt(this.generator, this._answers);

this.generator.args = this.args;
this.generator.arguments = this.args;
this.generator.options = _.extend({
'skip-install': true
}, this.options);

this.generator.once('end', this.emit.bind(this, 'end'));
this.emit('ready', this.generator);
this.generator.run();
};

Expand All @@ -79,9 +86,10 @@ RunContext.prototype._onReady = function () {
* @return {this}
*/

RunContext.prototype.inDir = function (dirPath) {
var release = this._holdExec();
helpers.testDirectory(dirPath, release);
RunContext.prototype.inDir = function (dirPath, cb) {
var release = this.async();
var callBackThenRelease = _.compose(release, (cb || _.noop).bind(this, path.resolve(dirPath)));
helpers.testDirectory(dirPath, callBackThenRelease);
return this;
};

Expand Down Expand Up @@ -114,17 +122,45 @@ RunContext.prototype.withOptions = function (options) {
*/

RunContext.prototype.withPrompt = function (answers) {
helpers.mockPrompt(this.generator, answers);
this._answers = answers;
return this;
};

/**
* Provide dependent generators
* @param {Array} dependencies - paths to the generators dependencies
* @return {this}
* @example
* var deps = ['../../common',
* '../../controller',
* '../../main',
* [helpers.createDummyGenerator(), 'testacular:app']
* ];
* var angular = new RunContext('../../app');
* angular.withGenerator(deps);
* angular.withPrompt({
* compass: true,
* bootstrap: true
* });
* angular.onEnd(function () {
* // assert something
* });
*/

RunContext.prototype.withGenerators = function (dependencies) {
assert(_.isArray(dependencies), 'dependencies should be an array');
this._dependencies = this._dependencies.concat(dependencies);
return this;
};

/**
* Add a callback to be called after the generator has ran
* @deprecated `onEnd` is deprecated, use .on('end', onEndHandler) instead.
* @param {Function} callback
* @return {this}
*/

RunContext.prototype.onEnd = function (cb) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to deprecate it, just delete.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

generator-karma is using it. Can we link so onEnd still works? If not, then this should be a point release and not a patch release

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then we should add a console.log deprecation notice.

this.generator.once('end', cb);
return this;
console.log('`onEnd` is deprecated, use .on(\'end\', onEndHandler) instead.');
return this.on('end', cb);
};
12 changes: 12 additions & 0 deletions test/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,18 @@ describe('yeoman.test', function () {
util.inherits(this.StubGenerator, yeoman.Base);
});

describe('.registerDependencies()', function () {
it('accepts dependency as a path', function () {
helpers.registerDependencies(env, ['./custom-generator-simple']);
assert(env.get('simple:app'));
});

it('accepts dependency as array of [<generator>, <name>]', function () {
helpers.registerDependencies(env, [[this.StubGenerator, 'stub:app']]);
assert(env.get('stub:app'));
});
});

describe('.createGenerator()', function () {
it('create a new generator', function () {
var generator = helpers.createGenerator('unicorn:app', [
Expand Down
96 changes: 74 additions & 22 deletions test/run-context.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,30 +18,36 @@ describe('RunContext', function () {
});

describe('constructor', function () {
it('accept path parameter', function () {
it('accept path parameter', function (done) {
var ctx = new RunContext(path.join(__dirname, './fixtures/custom-generator-simple'));
assert(ctx.env.get('simple:app'));
ctx.on('ready', function () {
assert(ctx.env.get('simple:app'));
done();
});
});

it('accept generator constructor parameter (and assign gen:test as namespace)', function () {
assert(this.ctx.env.get('gen:test'));
it('accept generator constructor parameter (and assign gen:test as namespace)', function (done) {
this.ctx.on('ready', function () {
assert(this.ctx.env.get('gen:test'));
done();
}.bind(this));
});

it('run the generator asynchronously', function (done) {
assert(this.execSpy.notCalled);
this.ctx.onEnd(function () {
assert(this.execSpy.calledOnce);
this.ctx.on('end', function () {
sinon.assert.calledOnce(this.execSpy);
done();
}.bind(this));
});

it('only run a generator once', function (done) {
this.ctx.onEnd(function () {
assert(this.execSpy.calledOnce);
this.ctx.on('end', function () {
sinon.assert.calledOnce(this.execSpy);
done();
}.bind(this));
this.ctx._onReady();
this.ctx._onReady();
this.ctx._run();
this.ctx._run();
});
});

Expand All @@ -61,20 +67,48 @@ describe('RunContext', function () {
it('is chainable', function () {
assert.equal(this.ctx.inDir(this.tmp), this.ctx);
});

it('accepts optional `cb` to be invoked with resolved `dir`', function (done) {
var ctx = new RunContext(this.Dummy);
var cb = sinon.spy(function () {
sinon.assert.calledOnce(cb);
sinon.assert.calledOn(cb, ctx);
sinon.assert.calledWith(cb, path.resolve(this.tmp));
done();
}.bind(this));
ctx.inDir(this.tmp, cb);
});

it('optional `cb` can use `this.async()` to delay execution', function (done) {
var ctx = new RunContext(this.Dummy);
var delayed = false;
var cb = sinon.spy(function () {
var release = this.async();
setTimeout(function () {
delayed = true;
release();
}.bind(this), 1);
});
ctx.inDir(this.tmp, cb)
.on('ready', function () {
assert(delayed);
done();
});
});
});

describe('#withArguments()', function () {
it('provide arguments to the generator when passed as Array', function (done) {
this.ctx.withArguments(['one', 'two']);
this.ctx.onEnd(function () {
this.ctx.on('end', function () {
assert.deepEqual(this.execSpy.firstCall.thisValue.arguments, ['one', 'two']);
done();
}.bind(this));
});

it('provide arguments to the generator when passed as String', function (done) {
this.ctx.withArguments('foo bar');
this.ctx.onEnd(function () {
this.ctx.on('end', function () {
assert.deepEqual(this.execSpy.firstCall.thisValue.arguments, ['foo', 'bar']);
done();
}.bind(this));
Expand All @@ -88,22 +122,22 @@ describe('RunContext', function () {
describe('#withOptions()', function () {
it('provide options to the generator', function (done) {
this.ctx.withOptions({ foo: 'bar' });
this.ctx.onEnd(function () {
this.ctx.on('end', function () {
assert.equal(this.execSpy.firstCall.thisValue.options.foo, 'bar');
done();
}.bind(this));
});

it('set skip-install by default', function (done) {
this.ctx.onEnd(function () {
this.ctx.on('end', function () {
assert.equal(this.execSpy.firstCall.thisValue.options['skip-install'], true);
done();
}.bind(this));
});

it('allow skip-install to be overriden', function (done) {
this.ctx.withOptions({ 'skip-install': false });
this.ctx.onEnd(function () {
this.ctx.on('end', function () {
assert.equal(this.execSpy.firstCall.thisValue.options['skip-install'], false);
done();
}.bind(this));
Expand Down Expand Up @@ -148,17 +182,35 @@ describe('RunContext', function () {
});
});

describe('#onEnd()', function () {
it('is called after the generator ran', function (done) {
assert(this.execSpy.notCalled);
this.ctx.onEnd(function () {
assert(this.execSpy.calledOnce);
describe('#withGenerators()', function () {
it('should register paths', function (done) {
this.ctx.withGenerators([
path.join(__dirname, './fixtures/custom-generator-simple')
]).on('ready', function () {
assert(this.ctx.env.get('simple:app'));
done();
}.bind(this));
});

it('is chainable', function () {
assert.equal(this.ctx.onEnd(function () {}), this.ctx);
it('should register mocked generator', function (done) {
this.ctx.withGenerators([
[helpers.createDummyGenerator(), 'dummy:gen']
]).on('ready', function () {
assert(this.ctx.env.get('dummy:gen'));
done();
}.bind(this));
});

it('should accumulate generators from multiple calls', function (done) {
this.ctx.withGenerators([
path.join(__dirname, './fixtures/custom-generator-simple')
]).withGenerators([
[helpers.createDummyGenerator(), 'dummy:gen']
]).on('ready', function () {
assert(this.ctx.env.get('dummy:gen'));
assert(this.ctx.env.get('simple:app'));
done();
}.bind(this));
});
});
});