c9-core/plugins/c9.ide.test/coverage.js

595 wiersze
22 KiB
JavaScript

define(function(require, exports, module) {
main.consumes = [
"Plugin", "test", "ui", "layout", "test.all", "c9", "util",
"tabManager", "commands", "settings", "Menu", "MenuItem", "Divider",
"preferences", "save", "test.all"
];
main.provides = ["test.coverage"];
return main;
function main(options, imports, register) {
var Plugin = imports.Plugin;
var test = imports.test;
var all = imports["test.all"];
var ui = imports.ui;
var c9 = imports.c9;
var util = imports.util;
var layout = imports.layout;
var commands = imports.commands;
var settings = imports.settings;
var tabManager = imports.tabManager;
var Menu = imports.Menu;
var MenuItem = imports.MenuItem;
var Divider = imports.Divider;
var save = imports.save;
var prefs = imports.preferences;
var testAll = imports["test.all"];
var Range = require("ace/range").Range;
/***** Initialization *****/
var plugin = new Plugin("Ajax.org", main.consumes);
var emit = plugin.getEmitter();
var files = {};
var tests = {};
var showCoverage = false;
var reWs = new RegExp("^" + util.escapeRegExp(c9.workspaceDir));
var menu, button;
function load() {
if (test.inactive)
return;
test.on("coverage", function(e) {
addToLibrary(e.node);
if (!showCoverage)
commands.exec("togglecoverage");
}, plugin);
test.on("clearCoverage", function(e) {
removeFromLibrary(e.node);
}, plugin);
test.on("clear", function() {
clear();
}, plugin);
// Hook opening of known files
tabManager.on("open", function(e) {
var tab = e.tab;
if (!showCoverage) return;
if (files[tab.path])
decorateTab(tab);
else if (tests[tab.path])
decorateTab(tab, true);
});
commands.addCommand({
name: "openrelatedtestfiles",
// hint: "runs the selected test(s) in the test panel with code coverage enabled",
// bindKey: { mac: "Command-O", win: "Ctrl-O" },
group: "Test",
exec: function(editor, args) {
var tree = test.focussedPanel.tree;
var fileNode = tree.selectedNode.findFileNode();
if (tests[fileNode.path]) {
tests[fileNode.path].paths.forEach(function(path) {
tabManager.openFile(path, true, function() {});
});
}
},
isAvailable: function() {
var tree = test.focussedPanel.tree;
if (!tree || !tree.selectedNode) return false;
var fileNode = tree.selectedNode.findFileNode();
return tests[fileNode.path] ? true : false;
}
}, plugin);
commands.addCommand({
name: "togglecoverage",
// hint: "runs the selected test(s) in the test panel with code coverage enabled",
// bindKey: { mac: "Command-O", win: "Ctrl-O" },
group: "Test",
exec: function() {
showCoverage = !showCoverage;
settings.set("state/test/coverage/@show", showCoverage);
if (!showCoverage)
clearAllDecorations();
else {
var tab;
for (var path in tests) {
tab = tabManager.findTab(path);
if (tab) decorateTab(tab, true);
}
for (var path in files) {
tab = tabManager.findTab(path);
if (tab) decorateTab(tab);
}
}
},
isAvailable: function() {
for (var path in tests) {
if (tests[path].all) return true;
}
return false;
}
}, plugin);
commands.addCommand({
name: "clearcoverage",
// hint: "runs the selected test(s) in the test panel with code coverage enabled",
// bindKey: { mac: "Command-O", win: "Ctrl-O" },
group: "Test",
exec: function() {
clear();
},
isAvailable: function() {
for (var path in tests) {
if (tests[path].all) return true;
}
return false;
}
}, plugin);
settings.on("read", function() {
settings.setDefaults("user/test/coverage", [
["alwayson", false],
["fullline", true],
["testfiles", false],
["toolbar", true]
]);
settings.setDefaults("state/test/coverage", [
["show", false]
]);
settings.set("state/test/coverage/@show", false);
var totalCoverage = settings.getNumber("state/test/coverage/@total");
if (totalCoverage && settings.getBool("user/test/coverage/@toolbar")) {
draw();
var amount = button.$ext.querySelector(".amount");
amount.textContent = totalCoverage + "%";
button.show();
}
all.once("draw", function() {
test.settingsMenu.append(new Divider({ position: 400 }));
test.settingsMenu.append(new MenuItem({
caption: "Show Coverage",
type: "check",
checked: "state/test/coverage/@show",
position: 500,
command: "togglecoverage"
}));
test.settingsMenu.append(new MenuItem({
caption: "Always Run With Code Coverage",
checked: "user/test/coverage/@alwayson",
type: "check",
position: 600
}));
test.settingsMenu.append(new MenuItem({
caption: "Mark Full Line Coverage In Editor",
checked: "user/test/coverage/@fullline",
type: "check",
position: 700
}));
});
}, plugin);
settings.on("user/test/coverage/@fullline", function(value) {
if (!showCoverage) return;
commands.exec("togglecoverage");
commands.exec("togglecoverage");
}, plugin);
settings.on("user/test/coverage/@testfiles", function(value) {
if (!showCoverage) return;
var tab;
if (value) {
for (var path in tests) {
tab = tabManager.findTab(path);
if (tab) decorateTab(tab);
}
}
else {
for (var path in tests) {
tab = tabManager.findTab(path);
if (tab) {
var session = tab.document.getSession().session;
if (session) clearDecoration(session);
}
}
}
}, plugin);
settings.on("user/test/coverage/@toolbar", function(value) {
value
? settings.getNumber("state/test/coverage/@total") && button.show()
: button.hide();
}, plugin);
prefs.add({
"Test": {
position: 1000,
"Code Coverage": {
position: 400,
"Always Run With Code Coverage": {
type: "checkbox",
position: 100,
setting: "user/test/coverage/@alwayson"
},
"Mark Full Line Coverage In Editor": {
type: "checkbox",
position: 200,
setting: "user/test/coverage/@fullline"
},
"Show Code Coverage In Test Files": {
type: "checkbox",
position: 300,
setting: "user/test/coverage/@testfiles"
},
"Show Total Code Coverage In Toolbar": {
type: "checkbox",
position: 400,
setting: "user/test/coverage/@toolbar"
},
}
}
}, plugin);
// Save hooks
// TODO figure out what changed in the file and only run applicable tests
save.on("afterSave", function(e) {
if (!settings.getBool("user/test/@runonsave") || !files[e.path])
return;
var tests = Object.keys(files[e.path].coverage).map(function(path) {
return all.findTest(path);
});
var cmd = files[e.path].coverage ? "runtestwithcoverage" : "runtest";
commands.exec(cmd, null, { nodes: tests });
}, plugin);
all.on("draw", function() {
var menuRelatedFiles = new Menu({}, plugin);
menuRelatedFiles.on("itemclick", function(e) {
tabManager.openFile(e.value, true);
});
menuRelatedFiles.on("show", function() {
var tree = test.focussedPanel.tree;
var fileNode = tree.selectedNode.findFileNode();
var items = menuRelatedFiles.items;
for (var i = items.length - 1; i > 0; i--) {
menuRelatedFiles.remove(items[i]);
}
if (tests[fileNode.path]) {
tests[fileNode.path].paths.forEach(function(path) {
menuRelatedFiles.append(new MenuItem({
caption: path,
value: path
}));
});
}
});
all.contextMenu.append(new MenuItem({
caption: "Open Related Files",
position: 450,
submenu: menuRelatedFiles,
isAvailable: function() {
var tree = test.focussedPanel.tree;
if (!tree || !tree.selectedNode) return false;
var fileNode = tree.selectedNode.findFileNode();
return tests[fileNode.path] ? true : false;
}
}), plugin);
});
}
var drawn;
function draw() {
if (drawn) return;
drawn = true;
menu = new Menu({ items: [
new MenuItem({
caption: "Show Coverage",
checked: "state/test/coverage/@show",
type: "check",
position: 500,
command: "togglecoverage"
}),
new MenuItem({
caption: "Clear",
command: "clearcoverage"
})
]}, plugin);
button = new ui.button({
"skin": "c9-simple-btn",
// "caption" : "Share",
"class": "coverage-btn",
"visible": false,
"submenu": menu.aml
});
ui.insertByIndex(layout.findParent({
name: "preferences"
}), button, 865, plugin);
// TODO threshold red: #AB4E4E, green: #3E713E
button.$ext.innerHTML = '\
<div class="title">Code coverage</div>\
<div class="amount">~</div>';
emit.sticky("draw");
}
/***** Methods *****/
function addToLibrary(node) {
var fileNode = node.findFileNode();
var coverageFiles = node.coverage.files;
if (!tests[fileNode.path] || !tests[fileNode.path].all) {
tests[fileNode.path] = {
paths: coverageFiles.map(function(coverage) {
return coverage.file.replace(reWs, "");
}),
node: fileNode,
all: coverageFiles
};
}
coverageFiles.forEach(function(coverage) {
var path = coverage.file.replace(reWs, "");
var tab;
if (tests[path]) {
tests[path].own = coverage;
tab = tabManager.findTab(path);
if (tab) decorateTab(tab, true);
}
else {
var fInfo = files[path];
if (!fInfo || !fInfo.coverage) {
fInfo = files[path] = {
coverage: {},
lines: {},
paths: [],
coveredLines: 0
};
}
var isNew = fInfo.coverage[fileNode.path] ? false : true;
if (!fInfo.coverage[fileNode.path])
fInfo.paths.push(fileNode.path);
fInfo.coverage[fileNode.path] = coverage;
fInfo.totalLines = coverage.lines.found;
if (isNew) {
fInfo.lines.covered = coverage.lines.covered;
fInfo.lines.uncovered = coverage.lines.uncovered;
}
else {
var covered = {}, uncovered = {};
for (var path in fInfo.coverage) {
var cvg = fInfo.coverage[path];
(cvg.lines.covered || []).forEach(function(nr) {
covered[nr] = true;
});
(cvg.lines.uncovered || []).forEach(function(nr) {
uncovered[nr] = true;
});
}
fInfo.lines = {
covered: Object.keys(covered),
uncovered: Object.keys(uncovered)
};
}
fInfo.coveredLines = fInfo.lines.covered.length;
tab = tabManager.findTab(path);
if (tab) decorateTab(tab);
}
});
// Store in settings
updateGlobalCoverage();
emit("update");
}
function removeFromLibrary(node) {
var path = typeof node == "string" ? node : node.findFileNode().path;
if (!tests[path]) return;
var all = tests[path].all;
var fileNode = tests[path].node;
delete tests[path].own;
delete tests[path].all;
delete tests[path].node;
if (fileNode) {
fileNode.coverage = null;
testAll.writeToCache(fileNode.findRunner(), path,
fileNode.serialize());
}
if (!all) return; // Already cleared
all.forEach(function(coverage) {
var path = coverage.file.replace(reWs, "");
var tab;
if (files[path]) {
var fInfo = files[path];
delete fInfo.coverage;
delete fInfo.lines;
}
tab = tabManager.findTab(path);
if (tab) clearDecoration(tab);
});
// updateGlobalCoverage();
emit("update");
}
function updateGlobalCoverage() {
var totalLines = 0, coveredLines = 0;
for (var path in files) {
var file = files[path];
totalLines += file.totalLines;
coveredLines += file.coveredLines;
}
draw();
var amount = button.$ext.querySelector(".amount");
var totalCoverage = Math.round(coveredLines / totalLines * 100);
amount.innerHTML = totalLines ? totalCoverage + "%" : "~";
button[totalLines ? "show" : "hide"]();
settings.set("state/test/coverage/@total", totalCoverage);
}
function addMarker(session, type, row, showMarker) {
var marker = showMarker
? session.addMarker(new Range(row, 0, row, 1), "test-" + type, "fullLine")
: null;
session.addGutterDecoration(row, type);
session.coverageLines.push({ marker: marker, gutter: row, type: type });
}
function decorateTab(tab, isTest) {
if (isTest && !tests[tab.path]) return;
else if (!files[tab.path]) return;
if (isTest && !settings.getBool("user/test/coverage/@testfiles"))
return;
var session = tab.document.getSession().session;
if (!session) {
tab.once("activate", function() {
setTimeout(function() {
decorateTab(tab, isTest);
});
});
return;
}
clearDecoration(session);
var coverage = isTest ? tests[tab.path].own : files[tab.path];
var showMarker = settings.getBool("user/test/coverage/@fullline");
if (coverage.lines.covered) {
coverage.lines.covered.forEach(function(row) {
addMarker(session, "covered", row - 1, showMarker);
});
}
if (coverage.lines.uncovered) {
coverage.lines.uncovered.forEach(function(row) {
addMarker(session, "uncovered", row - 1, showMarker);
});
}
}
function clearDecoration(session) {
if (session.document) {
session = session.document.getSession().session;
if (!session) return;
}
if (session.coverageLines) {
session.coverageLines.forEach(function(i) {
if (i.marker) session.removeMarker(i.marker);
session.removeGutterDecoration(i.gutter, i.type);
});
}
session.coverageLines = [];
}
function clearAllDecorations() {
tabManager.getTabs().forEach(function(tab) {
if (tab.editorType != "ace") return;
var session = tab.document.getSession().session;
if (session && session.coverageLines) clearDecoration(session);
});
showCoverage = false;
settings.set("state/test/coverage/@show", false);
}
function clear() {
for (var path in tests) {
removeFromLibrary(path);
}
// settings.set("state/test/coverage/@total", "");
// if (drawn) {
// button.hide();
// clearAllDecorations();
// }
emit("update");
}
/***** Lifecycle *****/
plugin.on("load", function() {
load();
});
plugin.on("unload", function() {
drawn = false;
files = null;
tests = null;
showCoverage = null;
menu = null;
button = null;
});
/***** Register and define API *****/
plugin.freezePublicAPI({
/**
*
*/
get buttonMenu() { return menu; },
/**
*
*/
get tests() { return tests; },
/**
*
*/
get files() { return files; },
/**
*
*/
clear: clear
});
register(null, {
"test.coverage": plugin
});
}
});