UNPKG

17 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 */
18
19var path = require('path');
20var net = require('net');
21var util = require('util');
22
23var sprintf = require('sprintf').sprintf;
24var async = require('async');
25var gex = require('gex');
26var underscore = require('underscore');
27var log = require('logmagic').local('whiskey.common');
28
29var assert = require('./assert');
30var constants = require('./constants');
31var testUtil = require('./util');
32var coverage = require('./coverage');
33var scopeleaks = require('./scopeleaks');
34
35var isValidTestFunctionName = function(name) {
36 return name.indexOf('test') === 0;
37};
38
39
40// foo.bar.test.* -> [ foo/bar/test.js, '*' ]
41// test. -> [ 'test.js', '*']
42// some.path.foo.test_bar* -> [ 'some/path/foo.js', 'test_bar*' ]
43function 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
60function SkipError(test, msg) {
61 this.test = test;
62 this.msg = msg || '';
63 Error.call(this, 'skipped');
64}
65
66util.inherits(SkipError, Error);
67
68var runInitFunction = function(filePath, callback) {
69 var testObj;
70 var initModule = null;
71
72 try {
73 initModule = require(filePath);
74 }
75 catch (err) {
76 // Invalid init file path provided
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
97function 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; // 'success' or 'failure'
108 this._err = null; // only populated if _status = 'failure'
109 this._skipMsg = null;
110 this._scopeLeaks = scopeLeaks || false;
111 this._leakedVariables = null; // a list of variables which leaked into
112 // global scope
113}
114
115Test.prototype._getScopeSnapshot = function(scope) {
116 if (!this._scopeLeaks) {
117 return null;
118 }
119
120 return scopeleaks.getSnapshot(scope);
121};
122
123Test.prototype._getLeakedVariables = function(scopeBefore, scopeAfter) {
124 if (!this._scopeLeaks) {
125 return null;
126 }
127
128 return scopeleaks.getDifferences(scopeBefore, scopeAfter);
129};
130
131Test.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 // someone called .finish() twice.
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
203Test.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
220Test.prototype._getAssertObject = function() {
221 return assert.getAssertModule(this);
222};
223
224Test.prototype._markAsSucceeded = function() {
225 this._finished = true;
226 this._status = 'success';
227};
228
229Test.prototype._markAsFailed = function(err) {
230 var stack;
231
232 if (err.hasOwnProperty('test')) {
233 delete err.test;
234 }
235
236 if (err.stack) {
237 // zomg, hacky, but in a later versions of V8 it looks like stack is some
238 // kind of special attribute which can't be serialized.
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
249Test.prototype._markAsSkipped = function(msg) {
250 this._finished = true;
251 this._status = 'skipped';
252 this._skipMsg = msg;
253};
254
255Test.prototype.isRunning = function() {
256 return !this._finished;
257};
258
259Test.prototype.beforeExit = function(handler) {
260 this._beforeExitHandler = handler;
261};
262
263Test.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
277function 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
294TestFile.prototype.addTest = function(test) {
295 this._tests.push(test);
296};
297
298TestFile.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 // Obtain the connection
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 // if test init file is present, run init function in it
340 function(callback) {
341 if (!self._testInitFile) {
342 callback();
343 return;
344 }
345
346 runInitFunction(self._testInitFile, callback);
347 },
348
349 // Require the test file
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 // if setUp function is present, run it
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 // Run the tests
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 // No test matched the provided pattern
428 callback();
429 }
430
431 },
432
433 // if tearDown function is present, run it
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
451TestFile.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
463TestFile.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
471TestFile.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
479TestFile.prototype._reportTestFileEnd = function() {
480 this._connection.end(sprintf('%s%s\n', this._filePath,
481 constants.TEST_FILE_END_MARKER));
482};
483
484TestFile.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 // User did not use our assert module or uncaughtException was thrown
501 // somewhere in the async code.
502 // Check which test is still running, mark it as failed and finish it.
503 test = this._runningTest;
504 test._markAsFailed(err);
505 test._testObj.finish();
506 this.addTest(test);
507 }
508 else {
509 // Can't figure out the origin, just add it to the _uncaughtExceptions
510 // array.
511 this._uncaughtExceptions.push(err);
512 }
513};
514
515TestFile.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
552function registerCustomAssertionFunctions(functions) {
553 assert.merge(null, functions);
554}
555
556exports.Test = Test;
557exports.TestFile = TestFile;
558exports.getTestFilePathAndPattern = getTestFilePathAndPattern;
559
560exports.registerCustomAssertionFunctions = registerCustomAssertionFunctions;
561
562/**
563 * @constructor
564 */
565function SpyOn (){
566 /**
567 * This tracks the arguments a function has been called with.
568 * @param {Object.<Array>}
569 * @private
570 */
571 this._calls = {};
572 /**
573 * This tracks the function to call
574 * @param {Object.<Function>}
575 * @private
576 */
577 this._funcMap = {};
578};
579
580/**
581 * @param {string} funcName The key to use for tracking call counts.
582 * @param {Object} context The context in which the function should execute.
583 * @param {Function} optFunc The optional function to call in wrapper. If not
584 * provided, original function will be called.
585 */
586SpyOn.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 * @param {string} funcName The key to clear.
608 * @param {Object} context The context in which the function should execute.
609 * @param {Function} optFunc The optional function to reapply to the context.
610 */
611SpyOn.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 * Reset a call count.
624 * @param {string} funcName The name of the function.
625 */
626SpyOn.prototype.reset = function (funcName) {
627 this._calls[funcName] = [];
628};
629
630/**
631 * Get the call count for a spied on function.
632 * @param {string} funcName
633 */
634SpyOn.prototype.called = function (funcName) {
635 var calls = this._calls[funcName];
636
637 // checks the actual args match the expected args
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};