UNPKG

12.7 kBJavaScriptView Raw
1'use strict';
2
3const EventEmitter = require('events').EventEmitter;
4const Bluebird = require('bluebird');
5const Path = require('path');
6const log = require('npmlog');
7const StyledString = require('styled_string');
8const find = require('lodash.find');
9
10const Server = require('./server');
11const BrowserTestRunner = require('./runners/browser_test_runner');
12const ProcessTestRunner = require('./runners/process_test_runner');
13const TapProcessTestRunner = require('./runners/tap_process_test_runner');
14const HookRunner = require('./runners/hook_runner');
15const cleanExit = require('./utils/clean_exit');
16const FileWatcher = require('./file_watcher');
17const LauncherFactory = require('./launcher-factory');
18
19const RunTimeout = require('./utils/run-timeout');
20const Reporter = require('./utils/reporter');
21const SignalListeners = require('./utils/signal-listeners');
22
23module.exports = class App extends EventEmitter {
24 constructor(config, finalizer) {
25 super();
26
27 this.exited = false;
28 this.paused = false;
29 this.config = config;
30 this.stdoutStream = config.get('stdout_stream') || process.stdout;
31 this.server = new Server(this.config);
32 this.results = [];
33 this.runnerIndex = 0;
34 this.runners = [];
35 this.timeoutID = undefined;
36 this.testSuiteTimedOut = null;
37 this.testSuiteTimedOut = false;
38
39 this.reportFileName = this.config.get('report_file');
40
41 let alreadyExit = false;
42
43 this.cleanExit = err => {
44 if (!alreadyExit) {
45 alreadyExit = true;
46
47 let exitCode = err ? 1 : 0;
48
49 if (err && err.hideFromReporter) {
50 err = null;
51 }
52
53 if (this.testSuiteTimedOut === true) {
54 let timeoutSeconds = this.testSuiteTimeout.timeout;
55 err = new Error(`Test suite execution has timed out (config.timeout = ${timeoutSeconds} seconds). Terminated all test runners.`);
56 exitCode = 1;
57 }
58
59 (finalizer || cleanExit)(exitCode, err);
60 }
61 };
62 }
63
64 start(cb) {
65 log.info('Starting ' + this.config.appMode);
66
67 return Bluebird.using(SignalListeners.with(), signalListeners => {
68 signalListeners.on('signal', err => this.exit(err));
69
70 return Bluebird.using(Reporter.with(this, this.stdoutStream, this.reportFileName), reporter => {
71 this.reporter = reporter;
72
73 return Bluebird.using(this.fileWatch(), () => {
74 return Bluebird.using(this.getServer(), () => {
75 return Bluebird.using(this.getRunners(), () => {
76 return Bluebird.using(this.runHook('on_start'), () => {
77 let w = this.waitForTests();
78
79 if (cb) {
80 cb();
81 }
82
83 return w;
84 }).then(() => {
85 log.info('Stopping ' + this.config.appMode);
86
87 this.emit('tests-finish');
88
89 return Bluebird.using(this.runHook('on_exit'), () => {});
90 }).catch(error => {
91 log.error(error);
92 log.info('Stopping ' + this.config.appMode);
93
94 this.emit('tests-error');
95
96 return new Bluebird((resolve, reject) => {
97 Bluebird.using(this.runHook('on_exit'), () => {}).then(() => {
98 reject(error);
99 });
100 });
101 });
102 });
103 });
104 });
105 });
106 }).asCallback(this.cleanExit);
107 }
108
109 waitForTests() {
110 log.info('Waiting for tests.');
111
112 if (this.exited) {
113 return Bluebird.reject(this.exitErr || new Error('Testem exited before running any tests.'));
114 }
115
116 let run = this.triggerRun('Start');
117
118 if (this.config.get('single_run')) {
119 run.then(() => this.exit());
120 }
121
122 return new Bluebird.Promise((resolve, reject) => {
123 this.on('testFinish', resolve);
124 this.on('testError', reject);
125 });
126 }
127
128 triggerRun(src) {
129 log.info(src + ' triggered test run.');
130
131 if (this.restarting) {
132 return;
133 }
134 this.restarting = true;
135
136 return this.stopCurrentRun().catch(this.exit.bind(this)).then(() => {
137 this.restarting = false;
138
139 return this.runTests();
140 });
141 }
142
143 stopCurrentRun() {
144 if (!this.currentRun) {
145 return Bluebird.resolve();
146 }
147
148 return Bluebird.all([ this.stopRunners(), this.currentRun ]);
149 }
150
151 runTests() {
152 if (this.paused) {
153 return Bluebird.resolve();
154 }
155
156 log.info('Running tests...');
157
158 this.reporter.onStart('testem', { launcherId: 0 });
159
160 return Bluebird.using(this.runHook('before_tests'), () => {
161 return Bluebird.using(RunTimeout.with(this.config.get('timeout')), timeout => {
162 this.testSuiteTimeout = timeout;
163
164 timeout.on('timeout', () => {
165 let timeoutSeconds = timeout.timeout;
166
167 log.info(`Test suite execution has timed out (config.timeout = ${timeoutSeconds} seconds). Terminating all test runners`);
168 this.testSuiteTimedOut = true;
169 this.killRunners();
170 });
171 this.timeoutID = timeout.timeoutID; // TODO Remove, just for the tests
172 this.currentRun = this.singleRun(timeout);
173 this.emit('testRun');
174
175 log.info('Tests running.');
176
177 return this.currentRun;
178 }).then(() => {
179 return Bluebird.using(this.runHook('after_tests'), () => {});
180 });
181 }).catch(err => {
182 if (err.hideFromReporter) {
183 return;
184 }
185
186 let result = {
187 failed: 1,
188 passed: 0,
189 name: 'testem',
190 launcherId: 0,
191 error: {
192 message: err.toString()
193 }
194 };
195
196 this.reporter.report('testem', result);
197 }).finally(() => this.reporter.onEnd('testem', { launcherId: 0 }));
198 }
199
200 exit(err, cb) {
201 err = err || this.getExitCode();
202
203 if (this.exited) {
204 if (cb) {
205 cb(err);
206 }
207 return;
208 }
209 this.exited = true;
210 this.exitErr = err;
211
212 if (err) {
213 this.emit('testError', err);
214 } else {
215 this.emit('testFinish');
216 }
217
218 if (cb) {
219 cb(err);
220 }
221 return;
222 }
223
224 startServer(callback) {
225 log.info('Starting server');
226 this.server = new Server(this.config);
227 this.server.on('file-requested', this.onFileRequested.bind(this));
228 this.server.on('browser-login', this.onBrowserLogin.bind(this));
229 this.server.on('browser-relogin', this.onBrowserRelogin.bind(this));
230 this.server.on('server-error', this.onServerError.bind(this));
231
232 return this.server.start().asCallback(callback);
233 }
234
235 getServer() {
236 return this.startServer().disposer(() => this.stopServer());
237 }
238
239 onFileRequested(filepath) {
240 if (this.fileWatcher && !this.config.get('serve_files')) {
241 this.fileWatcher.add(filepath);
242 }
243 }
244
245 onServerError(err) {
246 this.exit(err);
247 }
248
249 runHook(hook, data) {
250 return HookRunner.with(this.config, hook, data);
251 }
252
253 onBrowserLogin(browserName, id, socket) {
254 let browser = find(this.runners, runner => {
255 return runner.launcherId === id && (!runner.socket || !runner.socket.connected);
256 });
257
258 if (!browser) {
259 let launcher = new LauncherFactory(browserName, {
260 id: id,
261 protocol: 'browser'
262 }, this.config).create();
263 const singleRun = this.config.get('single_run');
264
265 browser = new BrowserTestRunner(launcher, this.reporter, this.runnerIndex++, singleRun, this.config);
266 this.addRunner(browser);
267 }
268
269 browser.tryAttach(browserName, id, socket);
270 }
271
272 onBrowserRelogin(browserName, id, socket) {
273 let browser = find(this.runners, runner => {
274 // a browser relogin can happen if a client socket was disconnected, which may not be reflected in runner.socket's connected state
275 // or if the socket was nulled by 'onDisconnect'
276 return runner.launcherId === id && (runner.socket || runner.socket === null);
277 });
278
279 if (!browser) {
280 log.warn(`Relogin from an unknown browser ${browserName} with id ${id}`);
281 return;
282 }
283
284 if (browser.socket !== null) {
285 browser.clearTimeouts();
286 } else {
287 browser.tryAttach(browserName, id, socket);
288 }
289 }
290
291 addRunner(runner) {
292 this.runners.push(runner);
293 this.emit('runnerAdded', runner);
294 }
295
296 fileWatch() {
297 return this.configureFileWatch().disposer(() => {});
298 }
299
300 configureFileWatch(cb) {
301 if (this.config.get('disable_watching')) {
302 return Bluebird.resolve().asCallback(cb);
303 }
304
305 this.fileWatcher = new FileWatcher(this.config);
306 this.fileWatcher.on('fileChanged', filepath => {
307 log.info(filepath + ' changed (' + (this.disableFileWatch ? 'disabled' : 'enabled') + ').');
308 if (this.disableFileWatch || this.paused) {
309 return;
310 }
311 let configFile = this.config.get('file');
312 if ((configFile && filepath === Path.resolve(configFile)) ||
313 (this.config.isCwdMode() && filepath === process.cwd())) {
314 // config changed
315 this.configure(() => {
316 this.triggerRun('Config changed');
317 });
318 } else {
319 Bluebird.using(this.runHook('on_change', {file: filepath}), () => {
320 this.triggerRun('File changed: ' + filepath);
321 });
322 }
323 });
324 this.fileWatcher.on('EMFILE', () => {
325 let view = this.view;
326 let text = [
327 'The file watcher received a EMFILE system error, which means that ',
328 'it has hit the maximum number of files that can be open at a time. ',
329 'Luckily, you can increase this limit as a workaround. See the directions below \n \n',
330 'Linux: http://stackoverflow.com/a/34645/5304\n',
331 'Mac OS: http://serverfault.com/a/15575/47234'
332 ].join('');
333 view.setErrorPopupMessage(new StyledString(text + '\n ').foreground('megenta'));
334 });
335
336 return Bluebird.resolve().asCallback(cb);
337 }
338
339 getRunners() {
340 return Bluebird.fromCallback(callback => {
341 this.createRunners(callback);
342 }).disposer(() => {
343 return this.killRunners();
344 });
345 }
346
347 createRunners(callback) {
348 let reporter = this.reporter;
349 this.config.getLaunchers((err, launchers) => {
350 if (err) {
351 return callback(err);
352 }
353
354 let testPages = this.config.get('test_page');
355 launchers.forEach((launcher) => {
356 for (let i = 0; i < testPages.length; i++) {
357 let launcherInstance = launcher.create({ test_page: testPages[i] });
358 let runner = this.createTestRunner(launcherInstance, reporter);
359 this.addRunner(runner);
360 }
361 });
362
363 callback(null);
364 });
365 }
366
367 getRunnerFactory(launcher) {
368 let protocol = launcher.protocol();
369 switch (protocol) {
370 case 'process':
371 return ProcessTestRunner;
372 case 'browser':
373 return BrowserTestRunner;
374 case 'tap':
375 return TapProcessTestRunner;
376 default:
377 throw new Error('Don\'t know about ' + protocol + ' protocol.');
378 }
379 }
380
381 createTestRunner(launcher, reporter) {
382 let singleRun = this.config.get('single_run');
383
384 return new (this.getRunnerFactory(launcher))(launcher, reporter, this.runnerIndex++, singleRun, this.config);
385 }
386
387 withTestTimeout() {
388 return this.startClock().disposer(() => {
389 return this.cancelExistingTimeout();
390 });
391 }
392
393 singleRun(timeout) {
394 let limit = this.config.get('parallel');
395
396 let options = {};
397
398 if (limit && limit >= 1) {
399 options.concurrency = parseInt(limit);
400 } else {
401 options.concurrency = Infinity;
402 }
403
404 return Bluebird.map(this.runners, (runner) => {
405 if (this.exited) {
406 let e = new Error('Run canceled.');
407 e.hideFromReporter = true;
408 return Bluebird.reject(e);
409 }
410 if (this.restarting) {
411 return Bluebird.resolve();
412 }
413 return timeout.try(() => runner.start());
414 }, options);
415 }
416
417 wrapUp(err) {
418 this.exit(err);
419 }
420
421 stopServer(callback) {
422 if (!this.server) {
423 return Bluebird.resolve().asCallback(callback);
424 }
425
426 return this.server.stop().asCallback(callback);
427 }
428
429 getExitCode() {
430 if (!this.reporter) {
431 return new Error('Failed to initialize.');
432 }
433 if (!this.reporter.hasPassed()) {
434 let e = new Error('Not all tests passed.');
435 e.hideFromReporter = true;
436 return e;
437 }
438 if (!this.reporter.hasTests() && this.config.get('fail_on_zero_tests')) {
439 return new Error('No tests found.');
440 }
441 return null;
442 }
443
444 stopRunners() {
445 return Bluebird.each(this.runners, runner => {
446 if (typeof runner.stop === 'function') {
447 return runner.stop();
448 }
449
450 return runner.exit();
451 });
452 }
453
454 killRunners() {
455 return Bluebird.each(this.runners, runner => runner.exit());
456 }
457
458 launchers() {
459 return this.runners.map(runner => runner.launcher);
460 }
461};