1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 |
|
19 | var path = require('path');
|
20 | var net = require('net');
|
21 | var util = require('util');
|
22 |
|
23 | var sprintf = require('sprintf').sprintf;
|
24 | var async = require('async');
|
25 | var gex = require('gex');
|
26 | var underscore = require('underscore');
|
27 | var log = require('logmagic').local('whiskey.common');
|
28 |
|
29 | var assert = require('./assert');
|
30 | var constants = require('./constants');
|
31 | var testUtil = require('./util');
|
32 | var coverage = require('./coverage');
|
33 | var scopeleaks = require('./scopeleaks');
|
34 |
|
35 | var isValidTestFunctionName = function(name) {
|
36 | return name.indexOf('test') === 0;
|
37 | };
|
38 |
|
39 |
|
40 |
|
41 |
|
42 |
|
43 | function getTestFilePathAndPattern(patternString) {
|
44 | if (/\.js$/.test(patternString)) {
|
45 | return [ patternString, '*' ];
|
46 | }
|
47 |
|
48 | var split = patternString.split('.');
|
49 | var len = split.length;
|
50 | var testFile, basePath, testPath, pattern;
|
51 |
|
52 | testFile = sprintf('%s.js', split[len - 2]);
|
53 | pattern = split[len - 1] || '*';
|
54 | basePath = split.splice(0, len - 2).join('/');
|
55 | testPath = path.join(basePath, testFile);
|
56 |
|
57 | return [ testPath, pattern ];
|
58 | }
|
59 |
|
60 | function SkipError(test, msg) {
|
61 | this.test = test;
|
62 | this.msg = msg || '';
|
63 | Error.call(this, 'skipped');
|
64 | }
|
65 |
|
66 | util.inherits(SkipError, Error);
|
67 |
|
68 | var runInitFunction = function(filePath, callback) {
|
69 | var testObj;
|
70 | var initModule = null;
|
71 |
|
72 | try {
|
73 | initModule = require(filePath);
|
74 | }
|
75 | catch (err) {
|
76 |
|
77 | callback();
|
78 | return;
|
79 | }
|
80 |
|
81 | if (initModule) {
|
82 | if (initModule.hasOwnProperty(constants.INIT_FUNCTION_NAME)) {
|
83 | try {
|
84 | initModule[constants.INIT_FUNCTION_NAME](callback);
|
85 | return;
|
86 | }
|
87 | catch (err2) {
|
88 | callback();
|
89 | return;
|
90 | }
|
91 | }
|
92 | }
|
93 |
|
94 | callback();
|
95 | };
|
96 |
|
97 | function Test(testName, testFunction, scopeLeaks) {
|
98 | this._testName = testName;
|
99 | this._testFunction = testFunction;
|
100 |
|
101 | this._finished = false;
|
102 | this._testObj = null;
|
103 | this._assertObj = null;
|
104 |
|
105 | this._timeStart = null;
|
106 | this._timeEnd = null;
|
107 | this._status = null;
|
108 | this._err = null;
|
109 | this._skipMsg = null;
|
110 | this._scopeLeaks = scopeLeaks || false;
|
111 | this._leakedVariables = null;
|
112 |
|
113 | }
|
114 |
|
115 | Test.prototype._getScopeSnapshot = function(scope) {
|
116 | if (!this._scopeLeaks) {
|
117 | return null;
|
118 | }
|
119 |
|
120 | return scopeleaks.getSnapshot(scope);
|
121 | };
|
122 |
|
123 | Test.prototype._getLeakedVariables = function(scopeBefore, scopeAfter) {
|
124 | if (!this._scopeLeaks) {
|
125 | return null;
|
126 | }
|
127 |
|
128 | return scopeleaks.getDifferences(scopeBefore, scopeAfter);
|
129 | };
|
130 |
|
131 | Test.prototype.run = function(callback) {
|
132 | var self = this;
|
133 | var scopeBefore, scopeAfter, err;
|
134 | var finishCallbackCalled = false;
|
135 |
|
136 | function finishCallback() {
|
137 | callback(self.getResultObject());
|
138 | }
|
139 |
|
140 | function finishFunc() {
|
141 | if (finishCallbackCalled) {
|
142 |
|
143 | log.infof('test.finish in [bold]${name}[/bold] has been called twice' +
|
144 | ', possible double callback in your code!',
|
145 | {'name': self._testName});
|
146 | return;
|
147 | }
|
148 |
|
149 | if (!self._status) {
|
150 | scopeAfter = self._getScopeSnapshot(global);
|
151 | self._leakedVariables = self._getLeakedVariables(scopeBefore, scopeAfter);
|
152 |
|
153 | self._markAsSucceeded();
|
154 | }
|
155 |
|
156 | self._timeEnd = new Date().getTime();
|
157 | finishCallbackCalled = true;
|
158 | finishCallback();
|
159 | }
|
160 |
|
161 | this._testObj = this._getTestObject(finishFunc);
|
162 | this._assertObj = this._getAssertObject();
|
163 |
|
164 | self._timeStart = new Date().getTime();
|
165 |
|
166 | try {
|
167 | scopeBefore = this._getScopeSnapshot(global);
|
168 | this._testFunction(this._testObj, this._assertObj);
|
169 |
|
170 | }
|
171 | catch (actualErr) {
|
172 | if ((actualErr instanceof Error)) {
|
173 | err = actualErr;
|
174 | }
|
175 | else if (underscore.isString(actualErr)) {
|
176 | err = {};
|
177 | err.message = actualErr;
|
178 | }
|
179 | else if (!actualErr.hasOwnProperty('message') && actualErr.toString &&
|
180 | typeof actualErr.toString === 'function') {
|
181 | err = {};
|
182 | err.message = actualErr.toString();
|
183 | }
|
184 | else {
|
185 | err = actualErr;
|
186 | }
|
187 |
|
188 | scopeAfter = this._getScopeSnapshot(global);
|
189 | this._leakedVariables = this._getLeakedVariables(scopeBefore, scopeAfter);
|
190 |
|
191 | if (err instanceof SkipError) {
|
192 | this._markAsSkipped(err.msg);
|
193 | }
|
194 | else {
|
195 | this._markAsFailed(err);
|
196 | }
|
197 |
|
198 | finishFunc();
|
199 | return;
|
200 | }
|
201 | };
|
202 |
|
203 | Test.prototype._getTestObject = function(finishFunc) {
|
204 | var self = this;
|
205 | var testObj = function test() {
|
206 | return finishFunc.apply(undefined, arguments);
|
207 | };
|
208 |
|
209 | function skipFunc(msg) {
|
210 | throw new SkipError(self, msg);
|
211 | }
|
212 |
|
213 | testObj.finish = finishFunc;
|
214 | testObj.skip = skipFunc;
|
215 | testObj.spy = new SpyOn();
|
216 |
|
217 | return testObj;
|
218 | };
|
219 |
|
220 | Test.prototype._getAssertObject = function() {
|
221 | return assert.getAssertModule(this);
|
222 | };
|
223 |
|
224 | Test.prototype._markAsSucceeded = function() {
|
225 | this._finished = true;
|
226 | this._status = 'success';
|
227 | };
|
228 |
|
229 | Test.prototype._markAsFailed = function(err) {
|
230 | var stack;
|
231 |
|
232 | if (err.hasOwnProperty('test')) {
|
233 | delete err.test;
|
234 | }
|
235 |
|
236 | if (err.stack) {
|
237 |
|
238 |
|
239 | stack = err.stack.toString();
|
240 | delete err.stack;
|
241 | err.stack = stack;
|
242 | }
|
243 |
|
244 | this._finished = true;
|
245 | this._err = err;
|
246 | this._status = 'failure';
|
247 | };
|
248 |
|
249 | Test.prototype._markAsSkipped = function(msg) {
|
250 | this._finished = true;
|
251 | this._status = 'skipped';
|
252 | this._skipMsg = msg;
|
253 | };
|
254 |
|
255 | Test.prototype.isRunning = function() {
|
256 | return !this._finished;
|
257 | };
|
258 |
|
259 | Test.prototype.beforeExit = function(handler) {
|
260 | this._beforeExitHandler = handler;
|
261 | };
|
262 |
|
263 | Test.prototype.getResultObject = function() {
|
264 | var resultObj = {
|
265 | 'name': this._testName,
|
266 | 'status': this._status,
|
267 | 'error': this._err,
|
268 | 'skip_msg': this._skipMsg,
|
269 | 'leaked_variables': this._leakedVariables,
|
270 | 'time_start': this._timeStart,
|
271 | 'time_end': this._timeEnd
|
272 | };
|
273 |
|
274 | return resultObj;
|
275 | };
|
276 |
|
277 | function TestFile(filePath, options) {
|
278 | this._filePath = filePath;
|
279 | this._pattern = options['pattern'];
|
280 |
|
281 | this._socketPath = options['socket_path'];
|
282 | this._fileName = path.basename(this._filePath);
|
283 | this._testInitFile = options['init_file'];
|
284 | this._timeout = options['timeout'];
|
285 | this._concurrency = options['concurrency'];
|
286 | this._scopeLeaks = options['scope_leaks'];
|
287 |
|
288 | this._tests = [];
|
289 | this._uncaughtExceptions = [];
|
290 |
|
291 | this._runningTest = null;
|
292 | }
|
293 |
|
294 | TestFile.prototype.addTest = function(test) {
|
295 | this._tests.push(test);
|
296 | };
|
297 |
|
298 | TestFile.prototype.runTests = function(callback) {
|
299 | var self = this;
|
300 | var i, test, exportedFunctions, exportedFunctionsNames, errName;
|
301 | var setUpFunc, tearDownFunc, setUpFuncIndex, tearDownFuncIndex;
|
302 | var testName, testFunc, testsLen;
|
303 | var callbackCalled = false;
|
304 | var testModule = this._filePath.replace(/\.js$/, '');
|
305 |
|
306 | function handleEnd() {
|
307 | var resultObj;
|
308 | if (callbackCalled) {
|
309 | return;
|
310 | }
|
311 |
|
312 | callbackCalled = true;
|
313 | callback();
|
314 | }
|
315 |
|
316 | function onTestDone(test, callback) {
|
317 | var resultObj = test.getResultObject();
|
318 |
|
319 | self.addTest(test);
|
320 | self._reportTestResult(resultObj);
|
321 | callback();
|
322 | }
|
323 |
|
324 | async.series([
|
325 |
|
326 | function(callback) {
|
327 | self._getConnection(function(err, connection) {
|
328 | if (err) {
|
329 | callback(new Error('Unable to establish connection with the master ' +
|
330 | 'process'));
|
331 | return;
|
332 | }
|
333 |
|
334 | self._connection = connection;
|
335 | callback();
|
336 | });
|
337 | },
|
338 |
|
339 |
|
340 | function(callback) {
|
341 | if (!self._testInitFile) {
|
342 | callback();
|
343 | return;
|
344 | }
|
345 |
|
346 | runInitFunction(self._testInitFile, callback);
|
347 | },
|
348 |
|
349 |
|
350 | function(callback) {
|
351 | var errName;
|
352 | try {
|
353 | exportedFunctions = require(testModule);
|
354 | }
|
355 | catch (err) {
|
356 | if (err.message.indexOf(testModule) !== -1 &&
|
357 | err.message.match(/cannot find module/i)) {
|
358 | errName = 'file_does_not_exist';
|
359 | }
|
360 | else {
|
361 | errName = 'uncaught_exception';
|
362 | }
|
363 |
|
364 | test = new Test(errName, null);
|
365 | test._markAsFailed(err);
|
366 | self._reportTestResult(test.getResultObject());
|
367 | callback(err);
|
368 | return;
|
369 | }
|
370 |
|
371 | exportedFunctionsNames = Object.keys(exportedFunctions);
|
372 | exportedFunctionsNames = exportedFunctionsNames.filter(isValidTestFunctionName);
|
373 | testsLen = exportedFunctionsNames.length;
|
374 | setUpFunc = exportedFunctions[constants.SETUP_FUNCTION_NAME];
|
375 | tearDownFunc = exportedFunctions[constants.TEARDOWN_FUNCTION_NAME];
|
376 |
|
377 | callback();
|
378 | },
|
379 |
|
380 |
|
381 | function(callback){
|
382 | if (!setUpFunc) {
|
383 | callback();
|
384 | return;
|
385 | }
|
386 |
|
387 | var test = new Test(constants.SETUP_FUNCTION_NAME, setUpFunc,
|
388 | self._scopeLeaks);
|
389 | test.run(async.apply(onTestDone, test, callback));
|
390 | },
|
391 |
|
392 |
|
393 | function(callback) {
|
394 | var queue;
|
395 |
|
396 | if (exportedFunctionsNames.length === 0) {
|
397 | callback();
|
398 | return;
|
399 | }
|
400 |
|
401 | function taskFunc(task, _callback) {
|
402 | var test = task.test;
|
403 | self._runningTest = test;
|
404 | test.run(async.apply(onTestDone, task.test, _callback));
|
405 | }
|
406 |
|
407 | function onDrain() {
|
408 | callback();
|
409 | }
|
410 |
|
411 | queue = async.queue(taskFunc, self._concurrency);
|
412 | queue.drain = onDrain;
|
413 |
|
414 | for (i = 0; i < testsLen; i++) {
|
415 | testName = exportedFunctionsNames[i];
|
416 | testFunc = exportedFunctions[testName];
|
417 |
|
418 | if (!gex(self._pattern).on(testName)) {
|
419 | continue;
|
420 | }
|
421 |
|
422 | test = new Test(testName, testFunc, self._scopeLeaks);
|
423 | queue.push({'test': test});
|
424 | }
|
425 |
|
426 | if (queue.length() === 0) {
|
427 |
|
428 | callback();
|
429 | }
|
430 |
|
431 | },
|
432 |
|
433 |
|
434 | function(callback) {
|
435 | if (!tearDownFunc) {
|
436 | callback();
|
437 | return;
|
438 | }
|
439 |
|
440 | var test = new Test(constants.TEARDOWN_FUNCTION_NAME, tearDownFunc,
|
441 | self._scopeLeaks);
|
442 | test.run(async.apply(onTestDone, test, callback));
|
443 | }
|
444 | ],
|
445 |
|
446 | function(err) {
|
447 | handleEnd();
|
448 | });
|
449 | };
|
450 |
|
451 | TestFile.prototype._getConnection = function(callback) {
|
452 | var connection = net.createConnection(this._socketPath);
|
453 |
|
454 | connection.on('connect', function onConnect() {
|
455 | callback(null, connection);
|
456 | });
|
457 |
|
458 | connection.on('error', function onError(err) {
|
459 | callback(err, null);
|
460 | });
|
461 | };
|
462 |
|
463 | TestFile.prototype._reportTestResult = function(resultObj) {
|
464 | var payload;
|
465 | payload = sprintf('%s%s%s%s%s\n', constants.TEST_START_MARKER, this._filePath,
|
466 | constants.DELIMITER, JSON.stringify(resultObj),
|
467 | constants.TEST_END_MARKER);
|
468 | this._connection.write(payload);
|
469 | };
|
470 |
|
471 | TestFile.prototype._reportTestCoverage = function(coverageObj) {
|
472 | this._connection.write(sprintf('%s%s%s%s\n',
|
473 | this._filePath,
|
474 | constants.DELIMITER,
|
475 | coverage.stringifyCoverage(coverageObj),
|
476 | constants.COVERAGE_END_MARKER));
|
477 | };
|
478 |
|
479 | TestFile.prototype._reportTestFileEnd = function() {
|
480 | this._connection.end(sprintf('%s%s\n', this._filePath,
|
481 | constants.TEST_FILE_END_MARKER));
|
482 | };
|
483 |
|
484 | TestFile.prototype.addUncaughtException = function(err) {
|
485 | var test = err.test;
|
486 |
|
487 | if (test) {
|
488 | if (err instanceof SkipError) {
|
489 | test._markAsSkipped(err.msg);
|
490 | test._testObj.finish();
|
491 | }
|
492 | else {
|
493 | test._markAsFailed(err);
|
494 | test._testObj.finish();
|
495 | }
|
496 |
|
497 | this.addTest(test);
|
498 | }
|
499 | else if (this._runningTest) {
|
500 |
|
501 |
|
502 |
|
503 | test = this._runningTest;
|
504 | test._markAsFailed(err);
|
505 | test._testObj.finish();
|
506 | this.addTest(test);
|
507 | }
|
508 | else {
|
509 |
|
510 |
|
511 | this._uncaughtExceptions.push(err);
|
512 | }
|
513 | };
|
514 |
|
515 | TestFile.prototype.getResultObject = function(errObj) {
|
516 | var i, test, result, name, uncaughtException;
|
517 | var testsLen = this._tests.length;
|
518 | var uncaughtExceptionsLen = this._uncaughtExceptions.length;
|
519 |
|
520 | var resultObj = {
|
521 | 'file_path': this._filePath,
|
522 | 'file_name': this._fileName,
|
523 | 'error': null,
|
524 | 'timeout': false,
|
525 | 'stdout': '',
|
526 | 'stderr': '',
|
527 | 'tests': {}
|
528 | };
|
529 |
|
530 | if (errObj) {
|
531 | resultObj.error = errObj;
|
532 | return resultObj;
|
533 | }
|
534 |
|
535 | for (i = 0; i < testsLen; i++) {
|
536 | test = this._tests[i];
|
537 | result = test.getResultObject();
|
538 | resultObj.tests[result.name] = result;
|
539 | }
|
540 |
|
541 | for (i = 0; i < uncaughtExceptionsLen; i++) {
|
542 | name = sprintf('uncaught_exception_%d', i + 1);
|
543 | uncaughtException = this._uncaughtExceptions[i];
|
544 | test = new Test(name, null);
|
545 | test._markAsFailed(uncaughtException);
|
546 | resultObj.tests[name] = test.getResultObject();
|
547 | }
|
548 |
|
549 | return resultObj;
|
550 | };
|
551 |
|
552 | function registerCustomAssertionFunctions(functions) {
|
553 | assert.merge(null, functions);
|
554 | }
|
555 |
|
556 | exports.Test = Test;
|
557 | exports.TestFile = TestFile;
|
558 | exports.getTestFilePathAndPattern = getTestFilePathAndPattern;
|
559 |
|
560 | exports.registerCustomAssertionFunctions = registerCustomAssertionFunctions;
|
561 |
|
562 |
|
563 |
|
564 |
|
565 | function SpyOn (){
|
566 | |
567 |
|
568 |
|
569 |
|
570 |
|
571 | this._calls = {};
|
572 | |
573 |
|
574 |
|
575 |
|
576 |
|
577 | this._funcMap = {};
|
578 | };
|
579 |
|
580 |
|
581 |
|
582 |
|
583 |
|
584 |
|
585 |
|
586 | SpyOn.prototype.on = function (funcName, context, optFunc) {
|
587 | var wrapper, func;
|
588 | if (optFunc) {
|
589 | func = optFunc;
|
590 | } else {
|
591 | func = context[funcName];
|
592 | }
|
593 | if (this._calls.hasOwnProperty(funcName)) {
|
594 | throw "Function already being tracked.";
|
595 | }
|
596 | this.reset(funcName);
|
597 | wrapper = (function () {
|
598 | this._calls[funcName].push(Array.prototype.slice.call(arguments));
|
599 | return func.apply(context, arguments);
|
600 | }).bind(this);
|
601 | context[funcName] = wrapper;
|
602 | this._funcMap[funcName] = func;
|
603 | return this;
|
604 | };
|
605 |
|
606 |
|
607 |
|
608 |
|
609 |
|
610 |
|
611 | SpyOn.prototype.clear = function (funcName, context, optFunc) {
|
612 | if (optFunc) {
|
613 | context[funcName] = optFunc
|
614 | } else {
|
615 | context[funcName] = this._funcMap[funcName];
|
616 | }
|
617 | delete this._calls[funcName];
|
618 | delete this._funcMap[funcName];
|
619 | return this;
|
620 | };
|
621 |
|
622 |
|
623 |
|
624 |
|
625 |
|
626 | SpyOn.prototype.reset = function (funcName) {
|
627 | this._calls[funcName] = [];
|
628 | };
|
629 |
|
630 |
|
631 |
|
632 |
|
633 |
|
634 | SpyOn.prototype.called = function (funcName) {
|
635 | var calls = this._calls[funcName];
|
636 |
|
637 |
|
638 | var checkArgsMatch = function (actualArgs, expectedArgs) {
|
639 | var i;
|
640 | if (actualArgs.length !== expectedArgs.length) {
|
641 | return false;
|
642 | }
|
643 | for (i = 0; i < expectedArgs.length; i++) {
|
644 | if (actualArgs[i] !== expectedArgs[i]) {
|
645 | return false;
|
646 | }
|
647 | }
|
648 | return true;
|
649 | };
|
650 |
|
651 | return {
|
652 | valueOf: function () {
|
653 | return calls.length;
|
654 | },
|
655 | withArgs: function () {
|
656 | var i, j, match;
|
657 | var args = Array.prototype.slice.call(arguments);
|
658 | for (i = 0; i < calls.length; i++) {
|
659 | if (checkArgsMatch(calls[i], args)) {
|
660 | return true;
|
661 | }
|
662 | }
|
663 | return false;
|
664 | },
|
665 | with: function () {
|
666 | return this.withArgs.apply(this, arguments);
|
667 | }
|
668 | }
|
669 | };
|