UNPKG

20 kBJavaScriptView Raw
1/*
2 * Licensed to Cloudkick, Inc ('Cloudkick') under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * Cloudkick licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18var util = require('util');
19var fs = require('fs');
20var path = require('path');
21var net = require('net');
22var spawn = require('child_process').spawn;
23var exec = require('child_process').exec;
24var execSync = require('child_process').execSync;
25
26var sprintf = require('sprintf').sprintf;
27var async = require('async');
28var logmagic = require('logmagic');
29var underscore = require('underscore');
30
31var constants = require('./constants');
32var parser = require('./parser');
33var common = require('./common');
34var testUtil = require('./util');
35var coverage = require('./coverage');
36var getReporter = require('./reporters/index').getReporter;
37var generateMakefile = require('./gen_makefile').generateMakefile;
38var _debugger = require('./debugger');
39var DebuggerInterface = _debugger.Interface;
40var ProcessRunner = require('./process_runner/runner').ProcessRunner;
41
42exports.exitCode = 0;
43exports.processRunner = null;
44
45function TestRunner(options) {
46 var ignored = function(ignored_test, ignored_assert) {};
47 var testReporter, coverageReporter, scopeLeaksReporter,
48 testReporterOptions, coverageReporterOptions, scopeLeaksReporterOptions,
49 defaultSocketPath;
50 var gst_exports;
51
52 defaultSocketPath = sprintf('%s-%s', constants.DEFAULT_SOCKET_PATH,
53 Math.random() * 10000);
54 this._tests = options['tests'];
55 this._independent_tests = options['independent-tests'];
56 this._max_suites = options['max-suites'];
57 this._dependencies = options['dependencies'] || null;
58 this._onlyEssential = options['only-essential'] || false;
59 this._socketPath = options['socket-path'] || defaultSocketPath;
60 this._coverage = options['coverage'];
61 this._scopeLeaks = options['scope-leaks'];
62 this._verbosity = options['verbosity'] || constants.DEFAULT_VERBOSITY;
63 this._failfast = options['failfast'] || false;
64 this._realTime = options['real-time'] || false;
65 this._debug = options['debug'];
66
67 this._global_setup_procedure = ignored;
68 this._global_teardown_procedure = ignored;
69
70 var global_setup_teardown_file = options['global-setup-teardown'] || null;
71 if (global_setup_teardown_file) {
72 if (global_setup_teardown_file.charAt(0) !== '/') {
73 global_setup_teardown_file = path.join(process.cwd(), global_setup_teardown_file);
74 }
75
76 gst_exports = require(global_setup_teardown_file);
77 if (gst_exports.hasOwnProperty(constants.GLOBAL_SETUP_FUNCTION_NAME)) {
78 if (underscore.isFunction(gst_exports.globalSetUp)) {
79 this._global_setup_procedure = gst_exports.globalSetUp;
80 }
81 }
82 if (gst_exports.hasOwnProperty(constants.GLOBAL_TEARDOWN_FUNCTION_NAME)) {
83 if (underscore.isFunction(gst_exports.globalTearDown)) {
84 this._global_teardown_procedure = gst_exports.globalTearDown;
85 }
86 }
87 }
88
89 testReporter = options['test-reporter'] || constants.DEFAULT_TEST_REPORTER;
90 testReporterOptions = {
91 'print_stdout': options['print-stdout'],
92 'print_stderr': options['print-stderr'],
93 'styles': (options['no-styles'] ? false : true),
94 'report_timing': options['report-timing']
95 };
96
97 this._testReporter = getReporter('test', testReporter, this._tests,
98 testReporterOptions);
99
100 if (this._coverage) {
101 coverageReporter = options['coverage-reporter'] || constants.DEFAULT_COVERAGE_REPORTER;
102 coverageReporterOptions = {
103 'directory': options['coverage-dir'],
104 'file': options['coverage-file']
105 };
106
107 this._coverageReporter = getReporter('coverage',
108 coverageReporter,
109 this._tests,
110 coverageReporterOptions);
111 }
112 else {
113 this._coverageReporter = null;
114 }
115
116 if (this._scopeLeaks) {
117 scopeLeaksReporter = options['scope-leaks-reporter'] || constants.DEFAULT_SCOPE_LEAKS_REPORTER;
118 scopeLeaksReporterOptions = {
119 'sequential': (options['concurrency'] === 1 || options['sequential'])
120 };
121 this._scopeLeaksReporter = getReporter('scope-leaks', scopeLeaksReporter,
122 this._tests,
123 scopeLeaksReporterOptions);
124 }
125 else {
126 this._scopeLeaksReporter = null;
127 }
128
129 this._server = null;
130 this._processRunner = null;
131 this._testFilesData = {};
132
133 this._completed = false;
134 this._forceStopped = false;
135 this._completedTests = [];
136}
137
138TestRunner.prototype.runTests = function(testInitFile, chdir,
139 customAssertModule,
140 timeout,
141 concurrency, failFast) {
142 var self = this, ops = [];
143 timeout = timeout || constants.DEFAULT_TEST_TIMEOUT;
144
145 function handleChildTimeout(child, filePath) {
146 var resultObj, testFile;
147
148 if (!child.killed) {
149 testFile = self._testFilesData[filePath];
150
151 resultObj = {
152 'tests': [],
153 'timeout': true
154 };
155
156 child.kill('SIGKILL');
157 self._handleTestResult(filePath, resultObj);
158 }
159
160 if (self._failfast) {
161 self.forceStop();
162 }
163 }
164
165 function runSuite(filePath, callback) {
166 var result, pattern, cwd, child, timeoutId, testFileData;
167
168 if (self._completed || self._forceStopped) {
169 callback(new Error('Runner has been stopped'));
170 return;
171 }
172
173 cwd = process.cwd();
174 result = common.getTestFilePathAndPattern(filePath);
175 filePath = result[0];
176 pattern = result[1];
177 filePath = (filePath.charAt(0) !== '/') ? path.join(cwd, filePath) : filePath;
178 child = self._spawnTestProcess(filePath, testInitFile, chdir,
179 customAssertModule, timeout,
180 concurrency, pattern);
181
182 if (!self._debug) {
183 timeoutId = setTimeout(function() {
184 handleChildTimeout(child, filePath, callback);
185 }, timeout);
186 }
187
188 self._testFilesData[filePath] = {
189 'child': child,
190 'callback': callback,
191 'timeout_id': timeoutId,
192 'stdout': '',
193 'stderr': ''
194 };
195
196 testFileData = self._testFilesData[filePath];
197
198 child.stdout.on('data', function(chunk) {
199 if (self._realTime) {
200 process.stdout.write(chunk.toString());
201 }
202 else {
203 testFileData['stdout'] += chunk;
204 }
205 });
206
207 child.stderr.on('data', function(chunk) {
208 if (self._realTime) {
209 process.stderr.write(chunk.toString());
210 }
211 else {
212 testFileData['stderr'] += chunk;
213 }
214 });
215
216 child.on('exit', function() {
217 clearTimeout(timeoutId);
218 self._handleTestFileEnd(filePath);
219 delete self._testFilesData[filePath];
220 });
221 }
222
223 function _run_global_setup() {
224 var test = new common.Test(constants.GLOBAL_SETUP_FUNCTION_NAME, self._global_setup_procedure, false);
225
226 test.run(function(results) {
227 if (results.status !== 'success') {
228 self._handleTestsCompleted();
229 // I don't know of a more elegant way to "fail early" when something goes wrong.
230 // Throwing the exception has the desired effect, but the visual output is ugly.
231 // Substance over flash strikes again! -- sam.falvo@rackspace.com
232 throw new Error('Exception while invoking globalSetUp procedure:\n'+JSON.stringify(results, null, 2));
233 }
234 });
235 }
236
237 function _run_global_teardown() {
238 var test = new common.Test(constants.GLOBAL_TEARDOWN_FUNCTION_NAME, self._global_teardown_procedure, false);
239
240 test.run(function(results) {
241 if (results.status !== 'success') {
242 console.error("WARNING: Failures detected in the globalTearDown procedure.\n"+JSON.stringify(results, null, 2));
243 }
244 });
245 }
246
247 function _run_all_suites() {
248 self._testReporter.handleTestsStart();
249 async.series([
250 async.forEachLimit.bind(null, self._independent_tests, self._max_suites, runSuite),
251 async.forEachSeries.bind(null, self._tests, runSuite)
252 ], self._handleTestsCompleted.bind(self));
253 }
254
255 function onBound() {
256 _run_global_setup();
257 _run_all_suites();
258 _run_global_teardown();
259 }
260
261 function startServer(callback) {
262 self._startServer(self._handleConnection.bind(self),
263 onBound);
264 }
265
266 function createRunner(callback) {
267 self._processRunner = new ProcessRunner(self._dependencies);
268 exports.processRunner = self._processRunner;
269 callback(null, null);
270 }
271
272 function findDependenciesToRun(_, callback) {
273 var cwd = process.cwd();
274 var testPaths = self._tests.map(function(testPath) {
275 testPath = common.getTestFilePathAndPattern(testPath)[0];
276 testPath = (testPath.charAt(0) !== '/') ? path.join(cwd, testPath) : testPath;
277 return testPath;
278 });
279
280 self._processRunner.findDependencies(testPaths, callback);
281 }
282
283 function startDependencies(dependencies, callback) {
284 self._processRunner.start(dependencies, callback);
285 }
286
287 if (self._dependencies) {
288 ops.push(createRunner);
289
290 if (self._onlyEssential) {
291 ops.push(findDependenciesToRun);
292 }
293
294 ops.push(startDependencies);
295 }
296
297 ops.push(startServer);
298
299 async.waterfall(ops, function(err) {
300 if (err) {
301 throw err;
302 }
303 });
304};
305
306TestRunner.prototype._spawnTestProcess = function(filePath,
307 testInitFile,
308 chdir,
309 customAssertModule,
310 timeout,
311 concurrency,
312 pattern) {
313 var cwd = process.cwd();
314 var libCovDir = (this._coverage) ? path.join(cwd, 'lib-cov') : null;
315 var runFilePath = path.join(__dirname, 'run_test_file.js');
316 var port = parseInt((Math.random() * (65000 - 2000) + 2000), 10);
317 var args = [];
318
319 if (this._debug) {
320 args.push('--debug-brk=' + port);
321 }
322
323 args = args.concat([runFilePath, filePath, this._socketPath, cwd, libCovDir,
324 this._scopeLeaks, chdir, customAssertModule, testInitFile,
325 timeout, concurrency, pattern]);
326 this._testReporter.handleTestFileStart(filePath);
327 var child = spawn(process.execPath, args);
328
329 if (this._debug) {
330 var debuggerInterface = new DebuggerInterface();
331 debuggerInterface.connect(port, 400, function(err, client) {
332 // Skip the breakpoint set by debug-brk
333 client.reqContinue();
334 });
335 }
336
337 return child;
338};
339
340TestRunner.prototype._addCompletedTest = function(filePath) {
341 this._completedTests.push(filePath);
342};
343
344TestRunner.prototype._handleTestsCompleted = function() {
345 var statusCode;
346
347 if (!this._completed || this._forceStopped) {
348 this._completed = true;
349 this._stopServer();
350
351 statusCode = this._testReporter.handleTestsComplete();
352
353 if (this._scopeLeaks) {
354 this._scopeLeaksReporter.handleTestsComplete();
355 }
356
357 if (this._coverage) {
358 this._coverageReporter.handleTestsComplete();
359 }
360
361 exports.exitCode = statusCode;
362
363 if (this._processRunner) {
364 this._processRunner.stop();
365 }
366
367 if (this._debug) {
368 process.exit();
369 }
370 }
371};
372
373TestRunner.prototype._handleTestResult = function(filePath, resultObj) {
374 this._testReporter.handleTestEnd(filePath, resultObj);
375
376 if (this._scopeLeaks) {
377 this._scopeLeaksReporter.handleTestEnd(filePath, resultObj);
378 }
379
380 if (this._failfast && resultObj['status'] === 'failure') {
381 this.forceStop();
382 }
383};
384
385TestRunner.prototype._handleTestCoverageResult = function(filePath, coverageData) {
386 var coverageObj = JSON.parse(coverageData);
387 this._coverageReporter.handleTestFileComplete(filePath, coverageObj);
388};
389
390TestRunner.prototype._handleTestFileEnd = function(filePath) {
391 var testData, coverageData, resultObj, coverageObj, split, testFile;
392 var stdout = '';
393 var stderr = '';
394
395 testFile = this._testFilesData[filePath];
396 if (testFile) {
397 stdout = testFile['stdout'];
398 stderr = testFile['stderr'];
399 }
400
401 this._testReporter.handleTestFileComplete(filePath, stdout, stderr);
402 this._addCompletedTest(filePath);
403
404 if (testFile) {
405 testFile['callback']();
406 }
407};
408
409TestRunner.prototype._handleConnection = function(connection) {
410 var self = this;
411 var data = '';
412 var lineProcessor = new testUtil.LineProcessor();
413 var dataString, endMarkIndex, testFile, testFileCallback, testFileTimeoutId;
414
415 function onLine(line) {
416 var result, filePath, end, resultObj;
417 result = testUtil.parseResultLine(line);
418 end = result[0];
419 filePath = result[1];
420 resultObj = result[2];
421
422 if (end) {
423 return;
424 }
425
426 if (resultObj.hasOwnProperty('coverage')) {
427 self._handleTestCoverageResult(filePath, resultObj['coverage']);
428 }
429 else {
430 self._handleTestResult(filePath, resultObj);
431 }
432 }
433
434 lineProcessor.on('line', onLine);
435
436 function onData(chunk) {
437 lineProcessor.appendData(chunk);
438 data += chunk;
439 }
440
441 connection.on('data', onData);
442};
443
444TestRunner.prototype._startServer = function(connectionHandler,
445 onBound) {
446 this._server = net.createServer(connectionHandler);
447 this._server.listen(this._socketPath, onBound);
448};
449
450TestRunner.prototype.forceStop = function() {
451 var testFile, testFileData, child, timeoutId;
452
453 this._forceStopped = true;
454 for (testFile in this._testFilesData) {
455 if (this._testFilesData.hasOwnProperty(testFile)) {
456 testFileData = this._testFilesData[testFile];
457 child = testFileData['child'];
458 timeoutId = testFileData['timeout_id'];
459 clearTimeout(timeoutId);
460 child.kill('SIGKILL');
461 }
462 }
463};
464
465TestRunner.prototype._stopServer = function() {
466 if (this._server) {
467 this._server.close();
468 this._server = null;
469 }
470};
471
472function run(cwd, argv) {
473 var customAssertModule, exportedFunctions;
474 var runner, testReporter, coverageArgs;
475 var socketPath, concurrency, scopeLeaks, runnerArgs;
476 var intersection;
477
478 if ((argv === undefined) && (cwd instanceof Array)) {
479 argv = cwd;
480 }
481
482 var p = parser.getParser(constants.WHISKEY_OPTIONS);
483 p.banner = 'Usage: whiskey [options] --tests "files"';
484 var options = parser.parseArgv(p, argv);
485
486 if (options['coverage-files']) {
487 var coverageReporter = options['coverage-reporter'] || constants.DEFAULT_COVERAGE_REPORTER;
488 var coverageReporterOptions = {
489 'directory': options['coverage-dir'],
490 'file': options['coverage-file']
491 };
492
493 this._coverageReporter = getReporter('coverage',
494 coverageReporter,
495 [],
496 coverageReporterOptions);
497 var coverageObj = coverage.aggregateCoverage(options['coverage-files'].split(','));
498 this._coverageReporter.handleTestsComplete(coverageObj);
499 }
500 else if ((options.tests && options.tests.length > 0) ||
501 (options['independent-tests'] && options['independent-tests'].length > 0)) {
502 options.tests = options.tests ? options.tests.split(' ') : [];
503 options['independent-tests'] = options['independent-tests'] ? options['independent-tests'].split(' ') : [];
504
505 var ms = parseInt(options['max-suites'], 10);
506 if (ms) {
507 options['max-suites'] = ms;
508 } else {
509 options['max-suites'] = 5;
510 }
511
512 intersection = underscore.intersection(options.tests, options['independent-tests']);
513 if (intersection.length > 0) {
514 util.puts(sprintf('The following tests cannot appear in both --tests and --independen-tests: %s', intersection));
515 process.exit(1);
516 }
517
518 if (options['debug'] && options.tests.length > 1) {
519 throw new Error('--debug option can currently only be used with a single test file');
520 }
521 else if (options['debug'] && options['independent-tests'].length > 1) {
522 throw new Error('--debug cannot be used with --independent-tests.');
523 }
524 else if (options['gen-makefile'] && options['makefile-path']) {
525 generateMakefile(options.tests, options['makefile-path'], function(err) {
526 if (err) {
527 throw err;
528 }
529
530 util.puts(sprintf('Makefile has been saved in %s.', options['makefile-path']));
531 });
532
533 return;
534 }
535
536 customAssertModule = options['custom-assert-module'];
537 if (customAssertModule) {
538 customAssertModule = (customAssertModule.charAt(0) !== '/') ? path.join(cwd, customAssertModule) : customAssertModule;
539
540 if (path.existsSync(customAssertModule)) {
541 customAssertModule = customAssertModule.replace(/$\.js/, '');
542 }
543 else {
544 customAssertModule = null;
545 }
546 }
547
548 concurrency = options['sequential'] ? 1 : options['concurrency'];
549 concurrency = concurrency || constants.DEFAULT_CONCURRENCY;
550 scopeLeaks = options['scope-leaks'];
551
552 options['print-stdout'] = (options['quiet']) ? false : true;
553 options['print-stderr'] = (options['quiet']) ? false : true;
554
555 if (options['quiet']) {
556 logmagic.registerSink('null', function nullLogger() {});
557 logmagic.route('__root__', logmagic.INFO, 'null');
558 }
559
560 runner = new TestRunner(options);
561 runnerArgs = [options['test-init-file'], options['chdir'],
562 customAssertModule, options['timeout'],
563 concurrency, options['fail-fast']];
564
565
566 if (options['coverage']) {
567 var nodePath = process.env['NODE_PATH'];
568
569 if (!nodePath || nodePath.indexOf('lib-cov') === -1) {
570 throw new Error('lib-cov is not in NODE_PATH. NODE_PATH environment variable' +
571 ' must contain lib-cov path for the coverage to work.');
572 }
573
574 coverageArgs = ['jscoverage'];
575
576 if (options['coverage-encoding']) {
577 coverageArgs.push(sprintf('--encoding=%s', options['coverage-encoding']));
578 }
579
580 if (options['coverage-exclude']) {
581 coverageArgs.push(sprintf('--exclude=%s', options['coverage-exclude']));
582 }
583
584 if (options['coverage-no-instrument']) {
585 coverageArgs.push(sprintf('--no-instrument=%s', options['coverage-no-instrument']));
586 }
587
588 coverageArgs.push(sprintf('lib %s', constants.COVERAGE_PATH));
589 coverageArgs = coverageArgs.join(' ');
590
591 if (!path.existsSync(path.join(process.cwd(), constants.COVERAGE_PATH)) || !options['coverage-no-regen']) {
592 exec(sprintf('rm -fr %s ; %s', constants.COVERAGE_PATH, coverageArgs), function(err) {
593 if (err) {
594 if (err.message.match(/jscoverage: not found/i)) {
595 err = new Error('jscoverage binary not found. To use test coverage ' +
596 ' you need to install node-jscoverag binary - ' +
597 'https://github.com/visionmedia/node-jscoverage');
598 }
599
600 throw err;
601 }
602
603 runner.runTests.apply(runner, runnerArgs);
604 });
605 }
606 else {
607 runner.runTests.apply(runner, runnerArgs);
608 }
609 }
610 else {
611 runner.runTests.apply(runner, runnerArgs);
612 }
613
614 }
615 else if (!p._halted) {
616 console.log(p.banner);
617 }
618
619 process.on('SIGINT', function onSigint() {
620 runner.forceStop();
621 });
622
623 process.on('exit', function() {
624 process.reallyExit(exports.exitCode);
625 });
626}
627
628exports.run = run;