Merge pull request #18 from spenceralger/master

Implemented code coverage
This commit is contained in:
spenceralger 2014-03-05 17:02:28 -07:00
commit 93488cf46c
25 changed files with 1334 additions and 128 deletions

View file

@ -17,7 +17,9 @@
"grunt-contrib-watch": "~0.5.3", "grunt-contrib-watch": "~0.5.3",
"grunt-contrib-jade": "~0.10.0", "grunt-contrib-jade": "~0.10.0",
"grunt-contrib-less": "~0.9.0", "grunt-contrib-less": "~0.9.0",
"grunt-cli": "~0.1.13" "grunt-cli": "~0.1.13",
"istanbul": "~0.2.4",
"path-browserify": "0.0.0"
}, },
"scripts": { "scripts": {
"test": "grunt test", "test": "grunt test",

View file

@ -286,5 +286,17 @@ define(function (require) {
this.fetch('doc'); this.fetch('doc');
}; };
// get the list of open data source objects
// primarily for testing purposes
Courier.prototype._openSources = function (type) {
if (!type) {
return _.transform(this._refs, function (open, refs) {
[].push.apply(open, refs);
}, []);
}
return this._refs[type] || [];
};
return Courier; return Courier;
}); });

View file

@ -15,7 +15,8 @@ require.config({
lodash: '../bower_components/lodash/dist/lodash', lodash: '../bower_components/lodash/dist/lodash',
moment: '../bower_components/moment/moment', moment: '../bower_components/moment/moment',
gridster: '../bower_components/gridster/dist/jquery.gridster', gridster: '../bower_components/gridster/dist/jquery.gridster',
config: '../config' configFile: '../config',
bower_components: '../bower_components'
}, },
shim: { shim: {
angular: { angular: {

View file

@ -1,20 +1,59 @@
module.exports = function (grunt) { module.exports = function (grunt) {
var instrumentationMiddleware = require('../utils/instrumentation');
var amdWrapMiddleware = require('../utils/amd-wrapper');
return { return {
dev: { dev: {
options: { options: {
base: '<%= src %>' middleware: function (connect, options, stack) {
stack = stack || [];
var root = grunt.config.get('root');
// when a request for an intrumented file comes in (?instrument=true)
// and it is included in `pattern`, it will be handled
// by this middleware
stack.push(instrumentationMiddleware({
// root that files should be served from
root: root,
// make file names easier to read
displayRoot: grunt.config.get('src'),
// filter the filenames that will be served
filter: function (filename) {
// return true if the filename should be
// included in the coverage report (results are cached)
return grunt.file.isMatch([
'**/src/**/*.js',
'!**/src/bower_components/**/*',
'!**/src/kibana/utils/{event_emitter,next_tick}.js'
], filename);
}
}));
// minimize code duplication (especially in the istanbul reporter)
// by allowing node_modules to be requested in an AMD wrapper
stack.push(amdWrapMiddleware({
root: root
}));
// standard static middleware reading from the root
stack.push(connect.static(root));
// allow browsing directories
stack.push(connect.directory(root));
// redirect requests for '/' to '/src/'
stack.push(function (req, res, next) {
if (req.url !== '/') return next();
res.statusCode = 303;
res.setHeader('Location', '/src/');
res.end();
});
return stack;
} }
},
test: {
options: {
base: [
'<%= unitTestDir %>',
'<%= testUtilsDir %>',
'<%= src %>',
'<%= root %>/node_modules/mocha',
'<%= root %>/node_modules/expect.js'
],
port: 8001
} }
} }
}; };

View file

@ -1,24 +1,41 @@
module.exports = function (grunt) { module.exports = function (grunt) {
var path = require('path');
return { return {
options: {
compileDebug: false
},
test: { test: {
src: [ files: {
'<%= unitTestDir %>/**/*.jade', '<%= unitTestDir %>/index.html': '<%= unitTestDir %>/index.jade'
'<%= app %>/partials/**/*.jade', },
'<%= app %>/apps/**/*.jade'
],
expand: true,
ext: '.html',
options: { options: {
data: function (src, dest) { data: function (src, dest) {
var pattern = grunt.config.process('<%= unitTestDir %>/**/*.js'); var unitTestDir = grunt.config.get('unitTestDir');
var tests = grunt.file.expand({}, pattern).map(function (filename) {
return filename.replace(grunt.config.get('unitTestDir'), '');
});
return { tests: JSON.stringify(tests) };
},
client: false
}
}
};
};
// filter for non unit test related files
if (!~path.dirname(src).indexOf(unitTestDir)) return;
var pattern = unitTestDir + '/specs/**/*.js';
var appdir = grunt.config.get('app');
return {
tests: grunt.file.expand({}, pattern).map(function (filename) {
return path.relative(appdir, filename).replace(/\.js$/, '');
})
};
}
}
},
clientside: {
files: {
'<%= testUtilsDir %>/istanbul_reporter/report.jade.js': '<%= testUtilsDir %>/istanbul_reporter/report.clientside-jade'
},
options: {
client: true,
amd: true,
namespace: false // return the template directly in the amd wrapper
}
}
};
};

View file

@ -3,7 +3,12 @@ module.exports = function (config) {
// just lint the source dir // just lint the source dir
source: { source: {
files: { files: {
src: ['Gruntfile.js', '<%= src %>/**/*.js', '<%= unitTestDir %>/**/*.js', '<%= root %>/tasks/**/*.js'] src: [
'Gruntfile.js',
'<%= src %>/**/*.js',
'<%= unitTestDir %>/**/*.js',
'<%= root %>/tasks/**/*.js'
]
} }
}, },
options: { options: {

View file

@ -1,8 +1,7 @@
module.exports = { module.exports = {
src: { src: {
src: [ src: [
'<%= app %>/styles/**/*.less', '<%= app %>/**/styles/**/*.less',
'<%= app %>/apps/**/*.less',
'!<%= src %>/**/_*.less' '!<%= src %>/**/_*.less'
], ],
expand: true, expand: true,

View file

@ -1,12 +1,14 @@
module.exports = { module.exports = {
unit: {
options: { options: {
log: true, log: true,
logErrors: true, logErrors: true,
urls: [
'http://localhost:8001/'
],
run: false run: false
},
unit: {
options: {
urls: [
'http://localhost:8000/test/unit/'
]
} }
} }
}; };

View file

@ -1,23 +1,29 @@
module.exports = function (grunt) { module.exports = function (grunt) {
return { return {
test: { test: {
files: ['<%= unitTestDir %>/*.jade', '<%= unitTestDir %>/**/*.js'], files: [
tasks: ['jade:test', 'mocha:unit'] '<%= unitTestDir %>/**/*.js'
],
tasks: ['mocha:unit']
}, },
less: { less: {
files: [ files: [
'<%= app %>/**/*.less', '<%= app %>/**/styles/**/*.less',
'<%= src %>/courier/**/*.less' '!<%= src %>/**/_*.less'
], ],
tasks: ['less'] tasks: ['less']
}, },
jade: { jade: {
files: [ files: [
'<%= app %>/**/*.jade', '<%= unitTestDir %>/index.jade'
'<%= src %>/courier/**/*.jade',
'!<%= unitTestDir %>/**/*.jade'
], ],
tasks: ['jade'] tasks: ['jade:test']
},
clientside_jade: {
files: [
'<%= testUtilsDir %>/istanbul_reporter/report.clientside-jade'
],
tasks: ['jade:clientside']
} }
}; };
}; };

View file

@ -1,3 +1,8 @@
module.exports = function (grunt) { module.exports = function (grunt) {
grunt.registerTask('dev', ['less', 'jade', 'connect:dev', 'watch']); grunt.registerTask('dev', [
'less',
'jade',
'connect:dev',
'watch'
]);
}; };

View file

@ -1,4 +1,3 @@
module.exports = function (grunt) { module.exports = function (grunt) {
grunt.registerTask('server', ['connect:dev:keepalive']); grunt.registerTask('server', ['connect:dev:keepalive']);
grunt.registerTask('test_server', ['connect:test:keepalive']);
}; };

View file

@ -2,13 +2,19 @@ module.exports = function (grunt) {
/* jshint scripturl:true */ /* jshint scripturl:true */
grunt.registerTask('test', [ grunt.registerTask('test', [
'jshint', 'jshint',
'connect:test', 'connect:dev',
'jade:test', 'jade',
'mocha:unit' 'mocha:unit'
]); ]);
grunt.registerTask('coverage', [
'blanket',
'connect:dev',
'mocha:coverage'
]);
grunt.registerTask('test:watch', [ grunt.registerTask('test:watch', [
'connect:test', 'connect:dev',
'watch:test' 'watch:test'
]); ]);
}; };

View file

@ -0,0 +1,26 @@
module.exports = function amdWrapMiddleware(opts) {
opts = opts || {};
var root = opts.root || '/';
var path = require('path');
var fs = require('fs');
var pathPrefix = opts.pathPrefix || '/amd-wrap/';
return function (req, res, next) {
// only allow prefixed requests
if (req.url.substring(0, pathPrefix.length) !== pathPrefix) return next();
// strip the prefix and form the filename
var filename = path.join(root, req._parsedUrl.pathname.replace('/amd-wrap/', ''));
fs.readFile(filename, 'utf8', function (err, contents) {
// file does not exist
if (err) return next(err.code === 'ENOENT' ? void 0 : err);
// respond with the wrapped code
res.statusCode = 200;
res.setHeader('Content-Type', 'application/javascript');
res.end('define(function (require, exports, module) {\n' + contents + '\n});');
});
};
};

View file

@ -0,0 +1,77 @@
var Istanbul = require('istanbul');
var i = new Istanbul.Instrumenter({
embedSource: true,
preserveComments: true,
noAutoWrap: true
});
module.exports = function instrumentationMiddleware(opts) {
var fs = require('fs');
var path = require('path');
// for root directory that files will be served from
var root = opts.root || '/';
// the root directory used to create a relative file path
// for display in coverage reports
var displayRoot = opts.displayRoot || null;
// filter the files in root that can be instrumented
var filter = opts.filter || function (filename) {
// by default only instrument *.js files
return /\.js$/.test(filename);
};
// cache filename resolution
var fileMap = {};
function filenameForReq(req) {
if (!~req.url.indexOf('instrument=true')) return false;
// expected absolute path to the file
var filename = path.join(root, req._parsedUrl.pathname);
// shortcut for dev where we could be reloading on every save
if (fileMap[filename] !== void 0) return fileMap[filename];
var ret = filename;
if (!fs.existsSync(filename) || !opts.filter(filename)) {
ret = false;
}
// cache the return value for next time
fileMap[filename] = ret;
return ret;
}
return function (req, res, next) {
// resolve the request to a readable filename
var filename = filenameForReq(req);
// the file either doesn't exist of it was filtered out by opts.filter
if (!filename) return next();
fs.readFile(filename, 'utf8', function (err, content) {
if (err) {
if (err.code !== 'ENOENT') {
// other issue, report!
return next(err);
}
// file was deleted, clear cache and move on
delete fileMap[filename];
return next();
}
res.statusCode = 200;
res.setHeader('Content-Type', 'application/javascript');
res.end(i.instrumentSync(
content,
// make file names easier to read
displayRoot ? path.relative(displayRoot, filename) : filename
));
});
};
};

View file

@ -1,8 +1,6 @@
{ {
"extends": "../src/.jshintrc", "extends": "../src/.jshintrc",
"white": false,
"globals": { "globals": {
"module": false, "module": false,
"inject": false, "inject": false,

View file

@ -1,9 +1,11 @@
<!DOCTYPE html><html><head><title>Kibana4 Tests</title><link rel="stylesheet" href="mocha.css"></head><body><div id="mocha"></div><script src="expect.js"></script><script src="mocha.js"></script><script>mocha.setup('bdd'); <!DOCTYPE html><html><head><title>Kibana4 Tests</title><link rel="stylesheet" href="/node_modules/mocha/mocha.css"></head><body><div id="mocha"></div><script src="/node_modules/expect.js/expect.js"></script><script src="/node_modules/mocha/mocha.js"></script><script src="/src/bower_components/requirejs/require.js"></script><script src="/src/kibana/require.config.js"></script><script type="text/javascript">window.COVERAGE = !!(/coverage=true/i.test(location.search));
// sauce labs & selenium inject global variables that break this mocha.setup('bdd');
// mocha.checkLeaks();
// mocha.globals(['mochaRunner', 'angular']);</script><script src="bower_components/requirejs/require.js"></script><script src="kibana/require.config.js"></script><script type="text/javascript">require.config({ require.config({
baseUrl: '/src/kibana',
paths: { paths: {
sinon: '../sinon' sinon: '../../test/utils/sinon',
istanbul_reporter: '../../test/utils/istanbul_reporter'
}, },
shim: { shim: {
'sinon/sinon': { 'sinon/sinon': {
@ -12,10 +14,29 @@
], ],
exports: 'sinon' exports: 'sinon'
} }
} },
// mark all requested files with instrument query param
urlArgs: COVERAGE ? 'instrument=true' : void 0
}); });
require(["/fixtures/field_mapping.js","/specs/apps/dashboard/index.js","/specs/apps/dashboard/mocks/modules.js","/specs/calculate_indices.js","/specs/courier.js","/specs/data_source.js","/specs/mapper.js"], function () {
function setupCoverage(done) {
document.title = document.title.replace('Tests', 'Coverage');
require(['istanbul_reporter/reporter'], function (IstanbulReporter) {
mocha.reporter(IstanbulReporter);
done();
});
}
function runTests() {
require(["../../test/unit/specs/apps/dashboard/index","../../test/unit/specs/apps/dashboard/mocks/modules","../../test/unit/specs/calculate_indices","../../test/unit/specs/courier","../../test/unit/specs/data_source","../../test/unit/specs/mapper"], function () {
window.mochaRunner = mocha.run().on('end', function () { window.mochaRunner = mocha.run().on('end', function () {
window.mochaResults = this.stats; window.mochaResults = this.stats;
}); });
});</script></body></html> });
}
if (COVERAGE) {
setupCoverage(runTests);
} else {
runTests();
}</script></body></html>

View file

@ -2,22 +2,22 @@ doctype html
html html
head head
title Kibana4 Tests title Kibana4 Tests
link(rel="stylesheet", href="mocha.css") link(rel="stylesheet", href='/node_modules/mocha/mocha.css')
body body
#mocha #mocha
script(src="expect.js") script(src='/node_modules/expect.js/expect.js')
script(src="mocha.js") script(src='/node_modules/mocha/mocha.js')
script. script(src='/src/bower_components/requirejs/require.js')
mocha.setup('bdd'); script(src='/src/kibana/require.config.js')
// sauce labs & selenium inject global variables that break this
// mocha.checkLeaks();
// mocha.globals(['mochaRunner', 'angular']);
script(src="bower_components/requirejs/require.js")
script(src="kibana/require.config.js")
script(type="text/javascript"). script(type="text/javascript").
window.COVERAGE = !!(/coverage=true/i.test(location.search));
mocha.setup('bdd');
require.config({ require.config({
baseUrl: '/src/kibana',
paths: { paths: {
sinon: '../sinon' sinon: '../../test/utils/sinon',
istanbul_reporter: '../../test/utils/istanbul_reporter'
}, },
shim: { shim: {
'sinon/sinon': { 'sinon/sinon': {
@ -26,10 +26,29 @@ html
], ],
exports: 'sinon' exports: 'sinon'
} }
} },
// mark all requested files with instrument query param
urlArgs: COVERAGE ? 'instrument=true' : void 0
}); });
require(!{tests}, function () {
function setupCoverage(done) {
document.title = document.title.replace('Tests', 'Coverage');
require(['istanbul_reporter/reporter'], function (IstanbulReporter) {
mocha.reporter(IstanbulReporter);
done();
});
}
function runTests() {
require(!{JSON.stringify(tests)}, function () {
window.mochaRunner = mocha.run().on('end', function () { window.mochaRunner = mocha.run().on('end', function () {
window.mochaResults = this.stats; window.mochaResults = this.stats;
}); });
}); });
}
if (COVERAGE) {
setupCoverage(runTests);
} else {
runTests();
}

View file

@ -4,7 +4,7 @@ define(function (require) {
var _ = require('lodash'); var _ = require('lodash');
var $ = require('jquery'); var $ = require('jquery');
var sinon = require('sinon/sinon'); var sinon = require('sinon/sinon');
var configFile = require('../../../config.js'); var configFile = require('configFile');
// Load the kibana app dependencies. // Load the kibana app dependencies.
require('angular-route'); require('angular-route');

View file

@ -13,7 +13,18 @@ define(function (require) {
expect(courier).to.be.a(Courier); expect(courier).to.be.a(Courier);
}); });
it('knows when a DataSource object has event listeners for the results event'); it('knows when a DataSource object has event listeners for the results event', function () {
var courier = new Courier();
var ds = courier.createSource('doc');
expect(courier._openSources('doc')).to.have.length(0);
ds.on('results', function () {});
expect(courier._openSources('doc')).to.have.length(1);
ds.removeAllListeners('results');
expect(courier._openSources('doc')).to.have.length(0);
});
it('executes queries on the interval for searches that have listeners for results'); it('executes queries on the interval for searches that have listeners for results');
describe('events', function () { describe('events', function () {

View file

@ -1,11 +1,11 @@
define(function (require) { define(function (require) {
var elasticsearch = require('../bower_components/elasticsearch/elasticsearch.js'); var elasticsearch = require('bower_components/elasticsearch/elasticsearch');
var _ = require('lodash'); var _ = require('lodash');
var sinon = require('sinon/sinon'); var sinon = require('sinon/sinon');
var Courier = require('courier/courier'); var Courier = require('courier/courier');
var DataSource = require('courier/data_source/data_source'); var DataSource = require('courier/data_source/data_source');
var Mapper = require('courier/mapper'); var Mapper = require('courier/mapper');
var fieldMapping = require('../fixtures/field_mapping.js'); var fieldMapping = require('../fixtures/field_mapping');
var client = new elasticsearch.Client({ var client = new elasticsearch.Client({
host: 'localhost:9200', host: 'localhost:9200',
@ -41,7 +41,9 @@ define(function (require) {
} }
}); });
sinon.stub(client, 'delete', function (params, callback) {callback(undefined,true);}); sinon.stub(client, 'delete', function (params, callback) {
callback(undefined, true);
});
}); });
afterEach(function () { afterEach(function () {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,239 @@
/**
* TODO: move this into it's own module, with it's own dependencies
*/
require.config({
paths: {
// jade runtime is required by the AMD wrapped jade templates as "jade"
jade: '/amd-wrap/node_modules/grunt-contrib-jade/node_modules/jade/runtime'
}
});
// fake fs module to make jade/runtime.js happy
define('fs', function () {});
// fake process obejct to make browserify-path happy
window.process = window.process || { cwd: function () { return '.'; }};
// the actual reporter module
define(function (require) {
var _ = require('lodash');
var $ = require('jquery');
var InsertionText = require('/amd-wrap/node_modules/istanbul/lib/util/insertion-text.js');
var objUtils = require('/amd-wrap/node_modules/istanbul/lib/object-utils.js');
// var annotate = require('/amd-wrap/node_modules/istanbul/lib/annotate.js');
var Progress = require('/amd-wrap/node_modules/mocha/lib/browser/progress.js');
var path = require('/amd-wrap/node_modules/path-browserify/index.js');
var template = require('./report.jade');
var Base = window.Mocha.reporters.Base;
function IstanbulReporter(runner) {
// "inherit" the base reporters characteristics
Base.call(this, runner);
var stats = this.stats;
var gotoFile = window.location.hash;
if (gotoFile) window.location.hash = '';
$(document.body)
.html('<center><canvas width="40" height="40"></canvas></center>');
var canvas = document.getElementsByTagName('canvas')[0];
var ctx;
var progress;
if (canvas.getContext) {
var ratio = window.devicePixelRatio || 1;
canvas.style.width = canvas.width;
canvas.style.height = canvas.height;
canvas.width *= ratio;
canvas.height *= ratio;
ctx = canvas.getContext('2d');
ctx.scale(ratio, ratio);
progress = new Progress();
progress.size(40);
}
runner.on('test end', function () {
if (progress) {
progress
.update((stats.tests / this.total) * 100 || 0)
.draw(ctx);
}
});
runner.on('end', function () {
var stats = _.omit(this.stats, 'start', 'end', 'suites');
stats['create report'] = Date.now();
$(document.body).empty().append($(createReport())); // attempt to force parsing immediately
stats['create report'] = Date.now() - stats['create report'];
toSec(stats, 'create report');
toSec(stats, 'duration');
linkNav();
show(stats);
if (gotoFile) {
var header = document.getElementById(gotoFile.substring(1));
if (header) {
window.location.hash = gotoFile;
document.body.scrollTop = header.offsetTop;
}
}
});
}
function createReport() {
var summary = objUtils.summarizeCoverage(window.__coverage__);
var dirs = _(window.__coverage__)
.map(convertFile)
.groupBy(function (file) {
var dir = path.dirname(file.filename);
return dir === '.' ? '' : dir;
})
.transform(function (dirs, files, dirname) {
_.each(files, function (file) {
file.relname = dirname ? path.relative(dirname, file.filename) : file.filename;
});
dirs.push({
name: dirname,
files: files,
coverage: _.reduce(files, function (sum, file) {
return sum + file.coverage;
}, 0) / files.length
});
}, [])
.sortBy('name')
.value();
return template({
cov: {
dirs: dirs,
coverage: summary.lines.pct,
sloc: summary.lines.total,
hits: summary.lines.covered,
misses: summary.lines.total - summary.lines.covered
},
dirname: path.dirname,
relative: path.relative,
coverageClass: function (coverage) {
if (coverage >= 75) return 'high';
if (coverage >= 50) return 'medium';
if (coverage >= 25) return 'low';
return 'terrible';
}
});
}
function convertFile(file) {
var summary = objUtils.summarizeFileCoverage(file);
var count = 0;
var structured = file.code.map(function (str) {
count += 1;
return {
line: count,
covered: null,
text: new InsertionText(str, true)
};
});
var html = '';
structured.unshift({
line: 0,
covered: null,
text: new InsertionText('')
});
_.forOwn(file.l, function (count, lineNumber) {
structured[lineNumber].covered = count > 0 ? true : false;
});
// annotate.Lines(file, structured);
//note: order is important, since statements typically result in spanning the whole line and doing branches late
//causes mismatched tags
// annotate.Branches(file, structured);
// annotate.Functions(file, structured);
// annotate.Statements(file, structured);
structured.shift();
var context = {
filename: file.path,
sloc: summary.lines.total,
coverage: summary.lines.pct,
hits: summary.lines.covered,
misses: summary.lines.total - summary.lines.covered,
source: _.map(structured, function (line, lineNumber) {
return {
coverage: file.l[line.line],
source: line.text + ''
};
})
};
return context;
// writer.write(detailTemplate(context));
// writer.write('</table></pre>\n');
// writer.write(footerTemplate(templateData));
}
function linkNav() {
var headings = $('h2').toArray();
$(window).scroll(function (e) {
var heading = find(window.scrollY);
if (!heading) return;
var links = document.querySelectorAll('#menu a')
, link;
for (var i = 0, len = links.length; i < len; ++i) {
link = links[i];
link.className = link.getAttribute('href') === '#' + heading.id
? 'active'
: '';
}
});
function find(y) {
var i = headings.length
, heading;
while (i--) {
heading = headings[i];
if (y >= heading.offsetTop) {
return heading;
}
}
}
}
function toSec(stats, prop) {
return stats[prop] = (stats[prop] / 1000).toFixed(2) + ' sec';
}
function show(info) {
var width = _(info).keys().sortBy('length').pop().length;
$('<pre>')
.addClass('coverage-stats')
.appendTo('#menu')
.text(
_.map(info, function (val, name) {
var row = val + ' - ' + name;
if (width - name.length) {
row += (new Array(width - name.length + 1)).join(' ');
}
return row;
}).join('\n')
);
}
return IstanbulReporter;
});