1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 | var util = require('util');
|
19 | var fs = require('fs');
|
20 | var path = require('path');
|
21 | var net = require('net');
|
22 | var spawn = require('child_process').spawn;
|
23 | var exec = require('child_process').exec;
|
24 | var execSync = require('child_process').execSync;
|
25 |
|
26 | var sprintf = require('sprintf').sprintf;
|
27 | var async = require('async');
|
28 | var logmagic = require('logmagic');
|
29 | var underscore = require('underscore');
|
30 |
|
31 | var constants = require('./constants');
|
32 | var parser = require('./parser');
|
33 | var common = require('./common');
|
34 | var testUtil = require('./util');
|
35 | var coverage = require('./coverage');
|
36 | var getReporter = require('./reporters/index').getReporter;
|
37 | var generateMakefile = require('./gen_makefile').generateMakefile;
|
38 | var _debugger = require('./debugger');
|
39 | var DebuggerInterface = _debugger.Interface;
|
40 | var ProcessRunner = require('./process_runner/runner').ProcessRunner;
|
41 |
|
42 | exports.exitCode = 0;
|
43 | exports.processRunner = null;
|
44 |
|
45 | function 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 |
|
138 | TestRunner.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 |
|
230 |
|
231 |
|
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 |
|
306 | TestRunner.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 |
|
333 | client.reqContinue();
|
334 | });
|
335 | }
|
336 |
|
337 | return child;
|
338 | };
|
339 |
|
340 | TestRunner.prototype._addCompletedTest = function(filePath) {
|
341 | this._completedTests.push(filePath);
|
342 | };
|
343 |
|
344 | TestRunner.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 |
|
373 | TestRunner.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 |
|
385 | TestRunner.prototype._handleTestCoverageResult = function(filePath, coverageData) {
|
386 | var coverageObj = JSON.parse(coverageData);
|
387 | this._coverageReporter.handleTestFileComplete(filePath, coverageObj);
|
388 | };
|
389 |
|
390 | TestRunner.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 |
|
409 | TestRunner.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 |
|
444 | TestRunner.prototype._startServer = function(connectionHandler,
|
445 | onBound) {
|
446 | this._server = net.createServer(connectionHandler);
|
447 | this._server.listen(this._socketPath, onBound);
|
448 | };
|
449 |
|
450 | TestRunner.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 |
|
465 | TestRunner.prototype._stopServer = function() {
|
466 | if (this._server) {
|
467 | this._server.close();
|
468 | this._server = null;
|
469 | }
|
470 | };
|
471 |
|
472 | function 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 |
|
628 | exports.run = run;
|