diff --git a/.travis.yml b/.travis.yml index 3c6757d..15275e0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,3 +2,5 @@ language: node_js node_js: - '0.10' - '0.11' + - '0.12' + - '4.1.1' diff --git a/bin/cli.js b/bin/cli.js index dade70a..eb25a12 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -20,6 +20,7 @@ help = '' + '\n -d, --deps dependency paths - files required before code (space separated)' + '\n -l, --log logging options, json have to be used' + '\n --cov create tests coverage report' + + '\n --timeout max block duration (in ms)' + '\n -h, --help show this help' + '\n -v, --version show module version' + '\n'; @@ -84,6 +85,9 @@ for (var key in args) { case '--paths': o.paths = args[key]; break; + case '--timeout': + o.maxBlockDuration = args[key]; + break; case '-v': case '--version': util.print( diff --git a/lib/child.js b/lib/child.js index c0dda7b..aa20d1b 100644 --- a/lib/child.js +++ b/lib/child.js @@ -12,14 +12,21 @@ var QUnit = require('qunitjs'), // http://GOESSNER.net/articles/JsonPath/ require('../support/json/cycle'); -var options = JSON.parse(process.argv[2]), +var options = JSON.parse(process.argv.pop()), currentModule = path.basename(options.code.path, '.js'), currentTest; +// send ping messages to when child is blocked. +// after I sent the first ping, testrunner will start to except the next ping +// within maxBlockDuration, otherwise this process will be killed +process.send({event: 'ping'}); +setInterval(function() { + process.send({event: 'ping'}); +}, Math.floor(options.maxBlockDuration / 2)); + process.on('uncaughtException', function(err) { if (QUnit.config.current) { QUnit.ok(false, 'Test threw unexpected exception: ' + err.message); - QUnit.start(); } process.send({ event: 'uncaughtException', @@ -30,9 +37,6 @@ process.on('uncaughtException', function(err) { }); }); -QUnit.config.autorun = false; -QUnit.config.autostart = false; - // make qunit api global, like it is in the browser _.extend(global, QUnit); @@ -54,8 +58,6 @@ function _require(res, addToGlobal) { _.extend(global, exports); } } - - QUnit.start(); } /** @@ -75,7 +77,7 @@ QUnit.testStart(function(test) { * @param {Object} data */ QUnit.log(function(data) { - data.test = this.config.current.testName; + data.test = QUnit.config.current.testName; data.module = currentModule; process.send({ event: 'assertionDone', @@ -168,3 +170,5 @@ _require(options.code, true); options.tests.forEach(function(test) { _require(test, false); }); + +QUnit.load(); \ No newline at end of file diff --git a/lib/coverage.js b/lib/coverage.js index 1c844f1..58ffd5e 100644 --- a/lib/coverage.js +++ b/lib/coverage.js @@ -4,7 +4,8 @@ var path = require('path'), var istanbul, collector, options = { - dir: 'coverage' + dir: 'coverage', + reporters: ['lcov', 'json'] }; try { @@ -38,7 +39,11 @@ exports.report = function() { if (collector) { Report = istanbul.Report; - reports = [Report.create('lcov', options), Report.create('json', options)]; + + reports = options.reporters.map(function (report) { + return Report.create(report, options); + }); + reports.forEach(function(rep) { rep.writeReport(collector, true); }); @@ -49,7 +54,16 @@ exports.instrument = function(options) { var matcher, instrumenter; matcher = function (file) { - return file === options.code.path; + var files = options.coverage.files; + if (files) { + files = Array.isArray(files) ? files : [files]; + return files.some(function(f) { + if (typeof f === 'string') return file.indexOf(f) === 0; + else throw new Error("invalid entry in options.coverage.files: " + typeof f); + }); + } else { + return file === options.code.path; + } } instrumenter = new istanbul.Instrumenter(); istanbul.hook.hookRequire(matcher, instrumenter.instrumentSync.bind(instrumenter)); diff --git a/lib/testrunner.js b/lib/testrunner.js index f7a7621..08d3f77 100644 --- a/lib/testrunner.js +++ b/lib/testrunner.js @@ -45,7 +45,10 @@ options = exports.options = { deps: null, // define namespace your code will be attached to on global['your namespace'] - namespace: null + namespace: null, + + // max amount of ms child can be blocked, after that we assume running an infinite loop + maxBlockDuration: 2000 }; /** @@ -55,40 +58,55 @@ options = exports.options = { */ function runOne(opts, callback) { var child; + var pingCheckTimeoutId; + var argv = process.argv.slice(); - child = cp.fork( - __dirname + '/child.js', - [JSON.stringify(opts)], - {env: process.env} - ); + argv.push(JSON.stringify(opts)); + child = cp.fork(__dirname + '/child.js', argv, {env: process.env}); function kill() { process.removeListener('exit', kill); child.kill(); } + function complete(err, data) { + kill(); + clearTimeout(pingCheckTimeoutId); + callback(err, data) + } + child.on('message', function(msg) { - if (msg.event === 'assertionDone') { - log.add('assertions', msg.data); - } else if (msg.event === 'testDone') { - log.add('tests', msg.data); - } else if (msg.event === 'done') { - msg.data.code = opts.code.path; - log.add('summaries', msg.data); - if (opts.coverage) { - coverage.add(msg.data.coverage); - msg.data.coverage = coverage.get(); - msg.data.coverage.code = msg.data.code; - log.add('coverages', msg.data.coverage); - } - if (opts.log.testing) { - console.log('done'); - } - callback(null, msg.data); - kill(); - } else if (msg.event === 'uncaughtException') { - callback(_.extend(new Error(), msg.data)); - kill(); + switch (msg.event) { + case 'ping': + clearTimeout(pingCheckTimeoutId); + pingCheckTimeoutId = setTimeout(function() { + complete(new Error('Process blocked for too long')); + }, opts.maxBlockDuration); + break; + case 'assertionDone': + log.add('assertions', msg.data); + break; + case 'testDone': + log.add('tests', msg.data); + break; + case 'done': + clearTimeout(pingCheckTimeoutId); + msg.data.code = opts.code.path; + log.add('summaries', msg.data); + if (opts.coverage) { + coverage.add(msg.data.coverage); + msg.data.coverage = coverage.get(); + msg.data.coverage.code = msg.data.code; + log.add('coverages', msg.data.coverage); + } + if (opts.log.testing) { + console.log('done'); + } + complete(null, msg.data); + break; + case 'uncaughtException': + complete(_.extend(new Error(), msg.data)); + break; } }); diff --git a/package.json b/package.json index 24455c4..48b0092 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "qunit", "description": "QUnit testing framework for nodejs", - "version": "0.7.2", + "version": "0.9.1", "author": "Oleg Slobodskoi ", "contributors": [ { @@ -30,7 +30,7 @@ "qunit": "./bin/cli.js" }, "engines": { - "node": ">=0.6.0 < 0.12.0" + "node": ">=0.6.0 < 5.0" }, "scripts": { "test": "node --harmony ./test/testrunner.js" @@ -39,7 +39,7 @@ "argsparser": "^0.0.6", "cli-table": "^0.3.0", "co": "^3.0.6", - "qunitjs": "1.10.0", + "qunitjs": "1.23.1", "tracejs": "^0.1.8", "underscore": "^1.6.0" }, diff --git a/readme.md b/readme.md index 35b52f5..3b85a90 100644 --- a/readme.md +++ b/readme.md @@ -1,5 +1,7 @@ ## QUnit testing framework for nodejs. +[![Build Status](https://travis-ci.org/kof/node-qunit.png?branch=master)](https://travis-ci.org/kof/node-qunit) + http://qunitjs.com http://github.com/jquery/qunit @@ -97,7 +99,10 @@ var testrunner = require("qunit"); deps: null, // define namespace your code will be attached to on global['your namespace'] - namespace: null + namespace: null, + + // max amount of ms child can be blocked, after that we assume running an infinite loop + maxBlockDuration: 2000 } ``` @@ -259,4 +264,10 @@ $ npm test ### Coverage -Code coverage via Istanbul. To utilize, install `istanbul` and set option `coverage: true` or give a path where to store report `coverage: {dir: "coverage/path"}` or pass `--cov` parameter in the shell. Coverage calculations based on code and tests passed to `node-qunit`. +Code coverage via Istanbul. + +To utilize, install `istanbul` and set option `coverage: true` or give a path where to store report `coverage: {dir: "coverage/path"}` or pass `--cov` parameter in the shell. + +To specify the format of coverage report pass reporters array to the coverage options: `coverage: {reporters: ['lcov', 'json']}` (default) + +Coverage calculations based on code and tests passed to `node-qunit`. diff --git a/test/fixtures/async-test.js b/test/fixtures/async-test.js index 92843fa..7bf59d8 100644 --- a/test/fixtures/async-test.js +++ b/test/fixtures/async-test.js @@ -1,27 +1,27 @@ -test('1', 1, function (){ - ok(true, "tests intermixing sync and async tests #1"); +test('1', 1, function (assert){ + assert.ok(true, "tests intermixing sync and async tests #1"); }); -test('a', 2, function(){ - stop(); +test('a', 2, function(assert){ + var done = assert.async(); setTimeout(function() { - ok(true, 'test a1'); - ok(true, 'test a2'); - start(); + assert.ok(true, 'test a1'); + assert.ok(true, 'test a2'); + done(); }, 10000); }); test('2', 1, function (){ - ok(true, "tests intermixing sync and async tests #2"); + assert.ok(true, "tests intermixing sync and async tests #2"); }); -test('b', 2, function(){ - stop(); +test('b', 2, function(assert){ + var done = assert.async(); setTimeout(function() { - ok(true, 'test b1'); - ok(true, 'test b2'); - start(); + assert.ok(true, 'test b1'); + assert.ok(true, 'test b2'); + done(); }, 10); }); diff --git a/test/fixtures/child-tests-global.js b/test/fixtures/child-tests-global.js index 7fbc84d..62996d4 100644 --- a/test/fixtures/child-tests-global.js +++ b/test/fixtures/child-tests-global.js @@ -1,4 +1,4 @@ -test("Dependency file required as global", function() { - equal(typeof whereFrom, "function"); - equal(whereFrom(), "I was required as global"); +test("Dependency file required as global", function(assert) { + assert.equal(typeof whereFrom, "function"); + assert.equal(whereFrom(), "I was required as global"); }); diff --git a/test/fixtures/child-tests-namespace.js b/test/fixtures/child-tests-namespace.js index 9857ed1..d3ba068 100644 --- a/test/fixtures/child-tests-namespace.js +++ b/test/fixtures/child-tests-namespace.js @@ -1,5 +1,5 @@ -test("Dependency file required as a namespace object", function() { - strictEqual(typeof testns != "undefined", true); - equal(typeof testns.whereFrom, "function", "right method attached to right object"); - equal(testns.whereFrom(), "I was required as a namespace object"); +test("Dependency file required as a namespace object", function(assert) { + assert.strictEqual(typeof testns != "undefined", true); + assert.equal(typeof testns.whereFrom, "function", "right method attached to right object"); + assert.equal(testns.whereFrom(), "I was required as a namespace object"); }); diff --git a/test/fixtures/coverage-multiple-code.js b/test/fixtures/coverage-multiple-code.js new file mode 100644 index 0000000..d21a63b --- /dev/null +++ b/test/fixtures/coverage-multiple-code.js @@ -0,0 +1 @@ +module.exports = require('./coverage-code') \ No newline at end of file diff --git a/test/fixtures/coverage-test.js b/test/fixtures/coverage-test.js index 52c67da..b94c2f6 100644 --- a/test/fixtures/coverage-test.js +++ b/test/fixtures/coverage-test.js @@ -1,15 +1,15 @@ -test('myMethod test', function() { - equal(myMethod(), 123, 'myMethod returns right result'); +test('myMethod test', function(assert) { + assert.equal(myMethod(), 123, 'myMethod returns right result'); }); -test('myAsyncMethod test', function() { - ok(true, 'myAsyncMethod started'); +test('myAsyncMethod test', function(assert) { + assert.ok(true, 'myAsyncMethod started'); - stop(); - expect(2); + var done = assert.async(); + assert.expect(2); myAsyncMethod(function(data) { - equal(data, 123, 'myAsyncMethod returns right result'); - start(); + assert.equal(data, 123, 'myAsyncMethod returns right result'); + done(); }); }); diff --git a/test/fixtures/generators-test.js b/test/fixtures/generators-test.js index 3d96e0a..1709d89 100644 --- a/test/fixtures/generators-test.js +++ b/test/fixtures/generators-test.js @@ -1,4 +1,4 @@ -test('generators', function* () { +test('generators', function* (assert) { var data = yield thunk(); - deepEqual(data, {a: 1}, 'woks'); + assert.deepEqual(data, {a: 1}, 'woks'); }); diff --git a/test/fixtures/infinite-loop-code.js b/test/fixtures/infinite-loop-code.js new file mode 100644 index 0000000..7d69e1c --- /dev/null +++ b/test/fixtures/infinite-loop-code.js @@ -0,0 +1 @@ +while(1) {} diff --git a/test/fixtures/infinite-loop-test.js b/test/fixtures/infinite-loop-test.js new file mode 100644 index 0000000..bcd690a --- /dev/null +++ b/test/fixtures/infinite-loop-test.js @@ -0,0 +1,3 @@ +test('infinite loop', function(assert) { + assert.ok(true) +}) diff --git a/test/fixtures/testrunner-tests.js b/test/fixtures/testrunner-tests.js index b5b3fc3..d4b1967 100644 --- a/test/fixtures/testrunner-tests.js +++ b/test/fixtures/testrunner-tests.js @@ -1,29 +1,29 @@ -test('myMethod test', function() { - equal(myMethod(), 123, 'myMethod returns right result'); - equal(myMethod(), 321, 'this should trigger an error'); +test('myMethod test', function(assert) { + assert.equal(myMethod(), 123, 'myMethod returns right result'); + assert.equal(myMethod(), 321, 'this should trigger an error'); }); -test('myAsyncMethod test', function() { - ok(true, 'myAsyncMethod started'); +test('myAsyncMethod test', function(assert) { + var done = assert.async(); + assert.expect(3); - stop(); - expect(3); + assert.ok(true, 'myAsyncMethod started'); myAsyncMethod(function(data) { - equal(data, 123, 'myAsyncMethod returns right result'); - equal(data, 321, 'this should trigger an error'); - start(); + assert.equal(data, 123, 'myAsyncMethod returns right result'); + assert.equal(data, 321, 'this should trigger an error'); + done(); }); }); -test('circular reference', function() { - equal(global, global, 'test global'); +test('circular reference', function(assert) { + assert.equal(global, global, 'test global'); }); -test('use original Date', function() { +test('use original Date', function(assert) { var timekeeper = require('timekeeper'); timekeeper.travel(Date.now() - 1000000); - ok(true, 'date modified'); + assert.ok(true, 'date modified'); }); diff --git a/test/testrunner.js b/test/testrunner.js index 536b7dd..5518d8b 100644 --- a/test/testrunner.js +++ b/test/testrunner.js @@ -153,6 +153,16 @@ chain.add('uncaught exception', function() { }); }); +chain.add('infinite loop', function() { + tr.run({ + code: fixtures + '/infinite-loop-code.js', + tests: fixtures + '/infinite-loop-test.js', + }, function(err, res) { + a.ok(err instanceof Error, 'error was forwarded'); + chain.next(); + }); +}); + chain.add('coverage', function() { tr.options.coverage = true; tr.run({ @@ -181,6 +191,40 @@ chain.add('coverage', function() { }); }); +chain.add('coverage-multiple', function() { + tr.options.coverage = true; + tr.run({ + code: fixtures + '/coverage-multiple-code.js', + tests: fixtures + '/coverage-test.js', + coverage: { + files: [ + fixtures + '/coverage-multiple-code.js', + fixtures + '/coverage-code.js' + ], + }, + }, function(err, res) { + var stat = { + files: 1, + tests: 2, + assertions: 3, + failed: 0, + passed: 3, + coverage: { + files: 1, + statements: { covered: 7, total: 8 }, + branches: { covered: 0, total: 0 }, + functions: { covered: 3, total: 4 }, + lines: { covered: 7, total: 8 } + } + }; + delete res.runtime; + a.equal(err, null, 'no errors'); + a.deepEqual(stat, res, 'coverage multiple code testing works'); + tr.options.coverage = false; + chain.next(); + }); +}); + if (generators.support) { chain.add('generators', function() { tr.run({