UNPKG

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