UNPKG

8.93 kBJavaScriptView Raw
1'use strict';
2var crypto = require('crypto');
3var path = require('path');
4var os = require('os');
5var assert = require('assert');
6var _ = require('lodash');
7var Promise = require('pinkie-promise');
8var yeoman = require('yeoman-environment');
9var util = require('util');
10var rimraf = require('rimraf');
11var EventEmitter = require('events').EventEmitter;
12var helpers = require('./');
13var TestAdapter = require('./adapter').TestAdapter;
14
15/**
16 * This class provide a run context object to façade the complexity involved in setting
17 * up a generator for testing
18 * @constructor
19 * @param {String|Function} Generator - Namespace or generator constructor. If the later
20 * is provided, then namespace is assumed to be
21 * 'gen:test' in all cases
22 * @param {Object} [settings]
23 * @param {Boolean} [settings.tmpdir=true] - Automatically run this generator in a tmp dir
24 * @param {Boolean} [settings.compatibility=false] - Run with yeoman-test 1.x compatibility
25 * @param {String} [settings.resolved] - File path to the generator (only used if Generator is a constructor)
26 * @param {String} [settings.namespace='gen:test'] - Namespace (only used if Generator is a constructor)
27 * @return {this}
28 */
29
30function RunContext(Generator, settings) {
31 this._asyncHolds = 0;
32 this.ran = false;
33 this.inDirSet = false;
34 this.args = [];
35 this.options = {};
36 this.answers = {};
37 this.localConfig = null;
38 this.dependencies = [];
39 this.Generator = Generator;
40 this.settings = _.extend(
41 { tmpdir: true, namespace: 'gen:test', compatibility: false },
42 settings
43 );
44 this.withOptions({
45 force: true,
46 skipCache: true,
47 skipInstall: true
48 });
49 setTimeout(this._run.bind(this), 10);
50}
51
52util.inherits(RunContext, EventEmitter);
53
54/**
55 * Hold the execution until the returned callback is triggered
56 * @return {Function} Callback to notify the normal execution can resume
57 */
58
59RunContext.prototype.async = function() {
60 this._asyncHolds++;
61
62 return function() {
63 this._asyncHolds--;
64 this._run();
65 }.bind(this);
66};
67
68/**
69 * Method called when the context is ready to run the generator
70 * @private
71 */
72
73RunContext.prototype._run = function() {
74 if (!this.inDirSet && this.settings.tmpdir) {
75 this.inTmpDir();
76 }
77
78 if (this._asyncHolds !== 0 || this.ran) {
79 return;
80 }
81
82 this.ran = true;
83 var namespace;
84 this.env = (this.envCB || (env => env)).call(
85 this,
86 yeoman.createEnv([], {}, new TestAdapter())
87 );
88
89 helpers.registerDependencies(this.env, this.dependencies);
90
91 if (_.isString(this.Generator)) {
92 namespace = this.env.namespace(this.Generator);
93 this.env.register(this.Generator);
94 } else {
95 namespace = this.settings.namespace;
96 this.env.registerStub(this.Generator, namespace, this.settings.resolved);
97 }
98
99 this.generator = this.env.create(namespace, {
100 arguments: this.args,
101 options: this.options
102 });
103
104 helpers.mockPrompt(this.generator, this.answers);
105
106 if (this.localConfig) {
107 // Only mock local config when withLocalConfig was called
108 helpers.mockLocalConfig(this.generator, this.localConfig);
109 }
110
111 this.generator.on(
112 'error',
113 function(err) {
114 if (!this.emit('error', err)) {
115 throw err;
116 }
117 }.bind(this)
118 );
119 this.generator.once(
120 'end',
121 function() {
122 helpers.restorePrompt(this.generator);
123 this.emit('end');
124 this.completed = true;
125 }.bind(this)
126 );
127
128 this.emit('ready', this.generator);
129
130 const generatorPromise = this.generator.run();
131 if (!this.settings.compatibility) {
132 generatorPromise.catch(err => this.emit('error', err));
133 }
134};
135
136/**
137 * Return a promise representing the generator run process
138 * @return {Promise} Promise resolved on end or rejected on error
139 */
140RunContext.prototype.toPromise = function() {
141 return new Promise(
142 function(resolve, reject) {
143 this.on(
144 'end',
145 function() {
146 resolve(this.targetDirectory);
147 }.bind(this)
148 );
149 this.on('error', reject);
150 }.bind(this)
151 );
152};
153
154/**
155 * Promise `.then()` duck typing
156 * @return {Promise}
157 */
158RunContext.prototype.then = function() {
159 var promise = this.toPromise();
160 return promise.then.apply(promise, arguments);
161};
162
163/**
164 * Promise `.catch()` duck typing
165 * @return {Promise}
166 */
167RunContext.prototype.catch = function() {
168 var promise = this.toPromise();
169 return promise.catch.apply(promise, arguments);
170};
171
172/**
173 * Clean the provided directory, then change directory into it
174 * @param {String} dirPath - Directory path (relative to CWD). Prefer passing an absolute
175 * file path for predictable results
176 * @param {Function} [cb] - callback who'll receive the folder path as argument
177 * @return {this} run context instance
178 */
179
180RunContext.prototype.inDir = function(dirPath, cb) {
181 this.inDirSet = true;
182 this.targetDirectory = dirPath;
183 var release = this.async();
184 var callBackThenRelease = _.flowRight(
185 release,
186 (cb || _.noop).bind(this, path.resolve(dirPath))
187 );
188 helpers.testDirectory(dirPath, callBackThenRelease);
189 return this;
190};
191
192/**
193 * Change directory without deleting directory content.
194 * @param {String} dirPath - Directory path (relative to CWD). Prefer passing an absolute
195 * file path for predictable results
196 * @return {this} run context instance
197 */
198RunContext.prototype.cd = function(dirPath) {
199 this.inDirSet = true;
200 this.targetDirectory = dirPath;
201 dirPath = path.resolve(dirPath);
202 try {
203 process.chdir(dirPath);
204 } catch (err) {
205 throw new Error(err.message + ' ' + dirPath);
206 }
207
208 return this;
209};
210
211/**
212 * Cleanup a temporary directy and change the CWD into it
213 *
214 * This method is called automatically when creating a RunContext. Only use it if you need
215 * to use the callback.
216 *
217 * @param {Function} [cb] - callback who'll receive the folder path as argument
218 * @return {this} run context instance
219 */
220RunContext.prototype.inTmpDir = function(cb) {
221 var tmpdir = path.join(os.tmpdir(), crypto.randomBytes(20).toString('hex'));
222 return this.inDir(tmpdir, cb);
223};
224
225/**
226 * Create an environment
227 *
228 * This method is called automatically when creating a RunContext. Only use it if you need
229 * to use the callback.
230 *
231 * @param {Function} [cb] - callback who'll receive the folder path as argument
232 * @return {this} run context instance
233 */
234RunContext.prototype.withEnvironment = function(cb) {
235 this.envCB = cb;
236 return this;
237};
238
239/**
240 * Clean the directory used for tests inside inDir/inTmpDir
241 */
242RunContext.prototype.cleanTestDirectory = function() {
243 if (this.targetDirectory) {
244 rimraf.sync(this.targetDirectory);
245 }
246};
247
248/**
249 * Provide arguments to the run context
250 * @param {String|Array} args - command line arguments as Array or space separated string
251 * @return {this}
252 */
253
254RunContext.prototype.withArguments = function(args) {
255 var argsArray = _.isString(args) ? args.split(' ') : args;
256 assert(
257 _.isArray(argsArray),
258 'args should be either a string separated by spaces or an array'
259 );
260 this.args = this.args.concat(argsArray);
261 return this;
262};
263
264/**
265 * Provide options to the run context
266 * @param {Object} options - command line options (e.g. `--opt-one=foo`)
267 * @return {this}
268 */
269
270RunContext.prototype.withOptions = function(options) {
271 // Add options as both kebab and camel case. This is to stay backward compatibles with
272 // the switch we made to meow for options parsing.
273 Object.keys(options).forEach(function(key) {
274 options[_.camelCase(key)] = options[key];
275 options[_.kebabCase(key)] = options[key];
276 });
277
278 this.options = _.extend(this.options, options);
279 return this;
280};
281
282/**
283 * Mock the prompt with dummy answers
284 * @param {Object} answers - Answers to the prompt questions
285 * @return {this}
286 */
287
288RunContext.prototype.withPrompts = function(answers) {
289 this.answers = _.extend(this.answers, answers);
290 return this;
291};
292
293/**
294 * Provide dependent generators
295 * @param {Array} dependencies - paths to the generators dependencies
296 * @return {this}
297 * @example
298 * var angular = new RunContext('../../app');
299 * angular.withGenerators([
300 * '../../common',
301 * '../../controller',
302 * '../../main',
303 * [helpers.createDummyGenerator(), 'testacular:app']
304 * ]);
305 * angular.on('end', function () {
306 * // assert something
307 * });
308 */
309
310RunContext.prototype.withGenerators = function(dependencies) {
311 assert(_.isArray(dependencies), 'dependencies should be an array');
312 this.dependencies = this.dependencies.concat(dependencies);
313 return this;
314};
315
316/**
317 * Mock the local configuration with the provided config
318 * @param {Object} localConfig - should look just like if called config.getAll()
319 * @return {this}
320 */
321RunContext.prototype.withLocalConfig = function(localConfig) {
322 assert(_.isObject(localConfig), 'config should be an object');
323 this.localConfig = localConfig;
324 return this;
325};
326
327module.exports = RunContext;