kopia lustrzana https://github.com/c9/core
1457 wiersze
53 KiB
JavaScript
1457 wiersze
53 KiB
JavaScript
define(function(require, exports, module) {
|
|
main.consumes = [
|
|
"TestPanel", "ui", "Tree", "settings", "panels", "commands", "test",
|
|
"Menu", "MenuItem", "Divider", "tabManager", "save", "preferences", "fs",
|
|
"run.gui", "layout", "c9", "Form"
|
|
];
|
|
main.provides = ["test.all"];
|
|
return main;
|
|
|
|
function main(options, imports, register) {
|
|
var TestPanel = imports.TestPanel;
|
|
var settings = imports.settings;
|
|
var panels = imports.panels;
|
|
var ui = imports.ui;
|
|
var Tree = imports.Tree;
|
|
var test = imports.test;
|
|
var commands = imports.commands;
|
|
var fs = imports.fs;
|
|
var c9 = imports.c9;
|
|
var Form = imports.Form;
|
|
var Menu = imports.Menu;
|
|
var MenuItem = imports.MenuItem;
|
|
var Divider = imports.Divider;
|
|
var tabManager = imports.tabManager;
|
|
var save = imports.save;
|
|
var layout = imports.layout;
|
|
var prefs = imports.preferences;
|
|
var runGui = imports["run.gui"];
|
|
|
|
var Node = test.Node;
|
|
|
|
var async = require("async");
|
|
var basename = require("path").basename;
|
|
var dirname = require("path").dirname;
|
|
var escapeHTML = require("ace/lib/lang").escapeHTML;
|
|
|
|
var LineWidgets = require("ace/line_widgets").LineWidgets;
|
|
var dom = require("ace/lib/dom");
|
|
// var Range = require("../range").Range;
|
|
|
|
/***** Initialization *****/
|
|
|
|
var plugin = new TestPanel("Ajax.org", main.consumes, {
|
|
caption: "All Tests",
|
|
index: 200,
|
|
// showTitle: true,
|
|
style: "flex:1;-webkit-flex:1"
|
|
});
|
|
var emit = plugin.getEmitter();
|
|
|
|
var tree, stopping, menuContext, running, boxFilter, menuInlineContext;
|
|
|
|
var wsNode = new Node({
|
|
label: "workspace",
|
|
isOpen: true,
|
|
className: "heading",
|
|
status: "loaded",
|
|
noSelect: true,
|
|
$sorted: true,
|
|
});
|
|
var rmtNode = new Node({
|
|
label: "remote",
|
|
isOpen: true,
|
|
className: "heading",
|
|
status: "loaded",
|
|
noSelect: true,
|
|
$sorted: true
|
|
});
|
|
var rootNode = new Node({
|
|
label: "root",
|
|
tree: tree,
|
|
items: [wsNode]
|
|
});
|
|
|
|
function load() {
|
|
if (test.inactive)
|
|
return;
|
|
|
|
panels.on("afterAnimate", function() {
|
|
if (panels.isActive("test"))
|
|
tree && tree.resize();
|
|
}, plugin);
|
|
|
|
test.on("ready", function() {
|
|
if (!test.config.excluded)
|
|
test.config.excluded = {};
|
|
if (!test.config.skipped)
|
|
test.config.skipped = {};
|
|
}, plugin);
|
|
|
|
test.on("updateConfig", function() {
|
|
async.each(test.runners, function(runner, cb) {
|
|
runner.update(cb);
|
|
}, function() {
|
|
refresh();
|
|
});
|
|
}, plugin);
|
|
|
|
settings.on("read", function() {
|
|
settings.setDefaults("user/test", [
|
|
["inlineresults", true],
|
|
["runonsave", true]
|
|
]);
|
|
}, plugin);
|
|
|
|
prefs.add({
|
|
"Test": {
|
|
position: 2000,
|
|
"Test Runner": {
|
|
position: 100,
|
|
"Run Tests On Save": {
|
|
type: "checkbox",
|
|
position: 50,
|
|
setting: "user/test/@runonsave"
|
|
},
|
|
"Show Inline Test Results": {
|
|
type: "checkbox",
|
|
position: 100,
|
|
setting: "user/test/@inlineresults"
|
|
},
|
|
"Exclude These Files": {
|
|
name: "txtTestExclude",
|
|
type: "textarea-row",
|
|
fixedFont: true,
|
|
width: 600,
|
|
height: 200,
|
|
rowheight: 250,
|
|
position: 1000
|
|
},
|
|
}
|
|
}
|
|
}, plugin);
|
|
|
|
plugin.getElement("txtTestExclude", function(txtTestExclude) {
|
|
var ta = txtTestExclude.lastChild;
|
|
|
|
ta.on("blur", function(e) {
|
|
test.config.excluded = {};
|
|
ta.value.split("\n").forEach(function(rawLine) {
|
|
var path = rawLine.split("#")[0].trim();
|
|
test.config.excluded[path] = rawLine;
|
|
});
|
|
test.saveConfig(function() {
|
|
// Trigger a refetch for all runners
|
|
test.refresh();
|
|
});
|
|
});
|
|
|
|
var update = function() {
|
|
var str = [];
|
|
for (var path in test.config.excluded) {
|
|
str.push(test.config.excluded[path]);
|
|
}
|
|
ta.setValue(str.join("\n"));
|
|
};
|
|
|
|
test.on("ready", update, plugin);
|
|
test.on("updateConfig", update, plugin);
|
|
}, plugin);
|
|
|
|
// Save hooks
|
|
save.on("afterSave", function(e) {
|
|
var runner = isTest(e.path, e.value);
|
|
if (!runner) return;
|
|
|
|
// Notify runners of change event and refresh tree
|
|
var runonsave = settings.getBool("user/test/@runonsave");
|
|
runner.fileChange({
|
|
path: e.path,
|
|
value: e.value,
|
|
runonsave: runonsave,
|
|
run: function(fileNode) {
|
|
// Re-run test on save
|
|
if (runonsave) {
|
|
var cmd = fileNode.coverage
|
|
? "runtestwithcoverage"
|
|
: "runtest";
|
|
|
|
fileNode.fixParents();
|
|
commands.exec(cmd, null, { nodes: [fileNode]});
|
|
}
|
|
},
|
|
refresh: function() {
|
|
tree && tree.refresh();
|
|
}
|
|
});
|
|
}, plugin);
|
|
|
|
// Run Button Hook
|
|
runGui.on("updateRunButton", function(e) {
|
|
if (!isTest(e.path)) return;
|
|
|
|
var btnRun = e.button;
|
|
btnRun.enable();
|
|
btnRun.setAttribute("command", "runfocussedtest");
|
|
btnRun.setAttribute("caption", "Run Test");
|
|
btnRun.setAttribute("tooltip", "Run Test" + basename(e.path));
|
|
|
|
return false;
|
|
}, plugin);
|
|
|
|
// Initiate test runners
|
|
test.on("register", function(e) { init(e.runner); }, plugin);
|
|
test.on("unregister", function(e) { deinit(e.runner); }, plugin);
|
|
|
|
test.on("update", function() {
|
|
test.runners.forEach(function(runner) {
|
|
updateStatus(runner.root, "loading");
|
|
runner.update();
|
|
});
|
|
}, plugin);
|
|
|
|
test.on("resize", function() {
|
|
tree && tree.resize();
|
|
}, plugin);
|
|
|
|
// This is global to protect from error states
|
|
plugin.on("stop", function() {
|
|
progress.stop = [];
|
|
running = false;
|
|
runGui.transformButton();
|
|
});
|
|
|
|
var label, form;
|
|
test.on("showRunMenu", function(e) {
|
|
if (!label) {
|
|
label = new ui.label({
|
|
caption: "general",
|
|
class: "runner-form-header"
|
|
});
|
|
|
|
var runner = e.runners[0] || 0;
|
|
|
|
form = new Form({
|
|
colwidth: 180,
|
|
rowheight: 45,
|
|
style: "width:320px",
|
|
form: [
|
|
{
|
|
title: "Parallel Test Execution",
|
|
type: "checked-spinner",
|
|
name: "parallel",
|
|
min: 1,
|
|
max: 999,
|
|
defaultCheckboxValue: test.config.parallel !== undefined
|
|
? test.config.parallel
|
|
: (runner.defaultParallel || false),
|
|
defaultValue: test.config.parallelConcurrency !== undefined
|
|
? test.config.parallelConcurrency
|
|
: (runner.defaultParallelConcurrency || 6),
|
|
onchange: function(e) {
|
|
test.config[e.type == "checkbox"
|
|
? "parallel"
|
|
: "parallelConcurrency"] = e.value;
|
|
test.saveConfig(function() {});
|
|
}
|
|
}
|
|
]
|
|
}, plugin);
|
|
}
|
|
e.menu.appendChild(label);
|
|
form.attachTo(e.menu);
|
|
|
|
return false;
|
|
});
|
|
|
|
// Initialize All Runners
|
|
test.runners.forEach(init);
|
|
|
|
// Set the all panel as the focussed panel
|
|
test.focussedPanel = plugin;
|
|
}
|
|
|
|
var drawn = false;
|
|
function draw(opts) {
|
|
if (drawn) return;
|
|
drawn = true;
|
|
|
|
// Insert CSS
|
|
ui.insertCss(require("text!./style.css"), options.staticPrefix, plugin);
|
|
|
|
// Tree
|
|
tree = new Tree({
|
|
container: opts.html,
|
|
scrollMargin: [10, 10],
|
|
theme: "filetree",
|
|
emptyMessage: "No tests found",
|
|
|
|
getCaptionHTML: function(node) {
|
|
if (node.type == "file") {
|
|
var path = dirname(node.label);
|
|
if (path == ".") return escapeHTML(node.label);
|
|
return escapeHTML(basename(path) + "/" + basename(node.label))
|
|
+ "<span class='extrainfo'> - " + escapeHTML(dirname(path)) + "</span>";
|
|
}
|
|
else if (node.type == "testset") {
|
|
return escapeHTML(node.label); // "<span style='opacity:0.5;'>" + escapeHTML(node.label) + "</span>";
|
|
}
|
|
else if (node.kind == "it") {
|
|
return "it " + escapeHTML(node.label);
|
|
}
|
|
else if (node.type == "runner") {
|
|
return escapeHTML(node.label) + " ("
|
|
+ (!node.items.length && node.status == "loading"
|
|
? "loading"
|
|
: node.items.length)
|
|
+ ")";
|
|
}
|
|
|
|
return escapeHTML(node.label);
|
|
},
|
|
|
|
getIconHTML: function(node) {
|
|
var icon = "default";
|
|
|
|
if (node.status === "loading") icon = "loading";
|
|
else if (node.status === "running") icon = "test-in-progress";
|
|
else if (node.passed === 1) icon = "test-passed";
|
|
else if (node.passed === 0) icon = "test-failed";
|
|
else if (node.passed === 2) icon = "test-error";
|
|
else if (node.passed === 3) icon = "test-terminated";
|
|
else if (node.skip) icon = "test-ignored";
|
|
else if (node.type == "testset") icon = "test-set";
|
|
else if (node.type == "file") icon = "test-file";
|
|
else if (node.type == "runner") icon = "test-file";
|
|
else if (node.type == "prepare") icon = "test-prepare";
|
|
else if (node.type == "test") icon = "test-notran";
|
|
|
|
return "<span class='ace_tree-icon filetree-icon " + icon + "'></span>";
|
|
},
|
|
|
|
getClassName: function(node) {
|
|
return (node.className || "")
|
|
+ (node.status == "loading" ? " loading" : "")
|
|
+ (node.status == "running" ? " loading" : ""); // TODO different running icon
|
|
},
|
|
|
|
getRowIndent: function(node) {
|
|
return node.$depth ? node.$depth - 1 : 0;
|
|
},
|
|
|
|
hasChildren: function(node) {
|
|
return node.status === "pending"
|
|
|| node.items && node.items.length;
|
|
},
|
|
|
|
loadChildren: function(node, callback) {
|
|
populate(node, callback);
|
|
},
|
|
|
|
sort: function(children) {
|
|
if (!children.length || children[0].type != "file")
|
|
return;
|
|
|
|
var compare = tree.model.alphanumCompare;
|
|
return children.sort(function(a, b) {
|
|
// TODO index sorting
|
|
// if (aIsSpecial && bIsSpecial) return a.index - b.index;
|
|
|
|
return compare(a.path + "", b.path + "");
|
|
});
|
|
}
|
|
}, plugin);
|
|
|
|
// TODO generalize this
|
|
tree.renderer.scrollBarV.$minWidth = 10;
|
|
|
|
tree.container.style.position = "absolute";
|
|
tree.container.style.left = "0";
|
|
tree.container.style.top = "0";
|
|
tree.container.style.right = "0";
|
|
tree.container.style.bottom = "0";
|
|
tree.container.style.height = "";
|
|
|
|
tree.setRoot(rootNode);
|
|
|
|
tree.commands.bindKey("Space", function(e) {
|
|
openTestFile();
|
|
});
|
|
|
|
tree.commands.bindKey("Enter", function(e) {
|
|
commands.exec("runtest");
|
|
});
|
|
|
|
tree.commands.bindKey("Shift-Enter", function(e) {
|
|
commands.exec("runtestwithcoverage");
|
|
});
|
|
|
|
tree.on("focus", function() {
|
|
test.focussedPanel = plugin;
|
|
});
|
|
|
|
tree.on("select", function() {
|
|
openTestFile([tree.selectedNode], true);
|
|
});
|
|
|
|
tree.on("afterChoose", function() {
|
|
commands.exec("runtest");
|
|
});
|
|
|
|
layout.on("eachTheme", function(e) {
|
|
var height = parseInt(ui.getStyleRule(".filetree .tree-row", "height"), 10) || 22;
|
|
tree.rowHeightInner = height;
|
|
tree.rowHeight = height;
|
|
|
|
if (e.changed && tree) tree.resize(true);
|
|
});
|
|
|
|
// Hook clear
|
|
test.on("clear", function() {
|
|
clear();
|
|
}, plugin);
|
|
|
|
// Hook opening of known files
|
|
tabManager.on("open", function(e) {
|
|
var node, tab = e.tab;
|
|
if (rootNode.findAllNodes("file").some(function(n) {
|
|
node = n;
|
|
return n.path == tab.path;
|
|
})) {
|
|
decorate(node, tab);
|
|
}
|
|
}, plugin);
|
|
|
|
// Filter
|
|
var toolbar = test.getElement("toolbar");
|
|
ui.insertByIndex(toolbar, new ui.filler(), 900, plugin);
|
|
boxFilter = ui.insertByIndex(toolbar, new apf.codebox({
|
|
"initial-message": "Filter Tests",
|
|
"clearbutton": true,
|
|
"focusselect": true,
|
|
"singleline": true,
|
|
"width": 100,
|
|
"style": "flex:10; max-width:150px"
|
|
// "style": "float:right;margin:1px 2px"
|
|
}), 1000, plugin);
|
|
boxFilter.ace.on("input", function() {
|
|
tree.filterKeyword = boxFilter.ace.getValue();
|
|
});
|
|
|
|
// Menu
|
|
menuContext = new Menu({ items: [
|
|
new MenuItem({ position: 100, command: "runtest", caption: "Run", class: "strong", hotkey: "Enter" }),
|
|
new MenuItem({ position: 200, command: "runtestwithcoverage", caption: "Run with Code Coverage", hotkey: "Shift-Enter" }),
|
|
new Divider({ position: 300 }),
|
|
new MenuItem({ position: 400, caption: "Open Test File", onclick: openTestFile, hotkey: "Space" }),
|
|
new MenuItem({ position: 500, caption: "Open Raw Test Output", command: "opentestoutput" }),
|
|
new Divider({ position: 600 }),
|
|
new MenuItem({ position: 700, caption: "Skip", command: "skiptest" }),
|
|
new MenuItem({ position: 800, caption: "Remove", command: "removetest" })
|
|
]}, plugin);
|
|
opts.aml.setAttribute("contextmenu", menuContext.aml);
|
|
|
|
menuInlineContext = new Menu({ items: [
|
|
new MenuItem({
|
|
caption: "Show Inline Test Results",
|
|
checked: "user/test/@inlineresults",
|
|
type: "check",
|
|
position: 100
|
|
}),
|
|
new Divider(),
|
|
new MenuItem({
|
|
caption: "Open Raw Test Output",
|
|
onclick: function() {
|
|
var path = tabManager.focussedTab.path;
|
|
var test = findTest(path);
|
|
if (test)
|
|
commands.exec("opentestoutput", null, { nodes: [test]});
|
|
},
|
|
position: 300
|
|
}),
|
|
new MenuItem({
|
|
caption: "Clear Test Results In This File",
|
|
onclick: function() {
|
|
var editor = tabManager.focussedTab.editor;
|
|
if (editor.ace)
|
|
clearDecoration(editor.ace.session);
|
|
},
|
|
position: 400
|
|
}),
|
|
]}, plugin);
|
|
|
|
settings.on("read", function() {
|
|
test.settingsMenu.append(new MenuItem({
|
|
caption: "Show Inline Test Results",
|
|
checked: "user/test/@inlineresults",
|
|
type: "check",
|
|
position: 100
|
|
}));
|
|
}, plugin);
|
|
|
|
settings.on("user/test/@inlineresults", function(value) {
|
|
rootNode.findAllNodes("file").forEach(function(fileNode) {
|
|
if (fileNode.passed === undefined) return;
|
|
var tab = tabManager.findTab(fileNode.path);
|
|
if (tab) decorate(fileNode, tab);
|
|
});
|
|
}, plugin);
|
|
|
|
tree.resize();
|
|
}
|
|
|
|
/***** Helper Methods *****/
|
|
|
|
function populate(node, callback, force) {
|
|
var runner = node.findRunner() || findFileByPath(node.path).findRunner();
|
|
|
|
updateStatus(node, "loading");
|
|
|
|
runner.populate(node, function(err) {
|
|
if (err) return callback(err); // TODO
|
|
|
|
updateStatus(node, "loaded");
|
|
node.fixParents();
|
|
|
|
if (node.skip) {
|
|
node.findAllNodes("test").forEach(function(n) {
|
|
n.skip = true;
|
|
});
|
|
}
|
|
|
|
callback();
|
|
});
|
|
}
|
|
|
|
function filter(path) {
|
|
return test.config ? test.config.excluded[path] : false;
|
|
}
|
|
|
|
function init(runner) {
|
|
if (!test.ready) return test.on("ready", init.bind(this, runner));
|
|
if (!test.drawn) return test.once("draw", init.bind(this, runner), runner);
|
|
|
|
var parent = runner.remote ? rmtNode : wsNode;
|
|
runner.root.parent = parent;
|
|
parent.items.push(runner.root);
|
|
|
|
if (wsNode.items.length == 1 && (!tree || !tree.selectedNode))
|
|
plugin.once("draw", function() { tree.select(runner.root); });
|
|
|
|
updateStatus(runner.root, "loading");
|
|
|
|
var first = true;
|
|
runner.init(filter, function(err) {
|
|
if (err) return console.error(err); // TODO
|
|
|
|
runner.root.isOpen = true;
|
|
updateStatus(runner.root, "loaded");
|
|
|
|
runner.root.findAllNodes("file").forEach(function(node) {
|
|
// Mark skipped tests
|
|
if (test.config.skipped[node.path]) {
|
|
node.skip = true;
|
|
node.findAllNodes("test").forEach(function(n) {
|
|
n.skip = true;
|
|
});
|
|
}
|
|
|
|
// Call Results
|
|
if (first) {
|
|
if (typeof node.passed == "number")
|
|
emit("result", { node: node });
|
|
|
|
if (node.coverage)
|
|
test.setCoverage(node);
|
|
}
|
|
});
|
|
|
|
runner.root.fixParents();
|
|
|
|
if (first) {
|
|
// Init any tab that is already opened
|
|
tabManager.getTabs().forEach(function(tab) {
|
|
if (tab.path && isTest(tab.path, tab.document.value)) {
|
|
decorate(findFileByPath(tab.path), tab);
|
|
}
|
|
});
|
|
}
|
|
|
|
first = false;
|
|
});
|
|
}
|
|
|
|
function deinit(runner) {
|
|
if (runner.root.parent) {
|
|
var items = runner.root.parent.items;
|
|
items.splice(items.indexOf(runner.root), 1);
|
|
}
|
|
|
|
tree.refresh();
|
|
}
|
|
|
|
function findFileByPath(path) {
|
|
var found = false;
|
|
rootNode.findAllNodes("file").some(function(n) {
|
|
if (n.path == path) {
|
|
found = n;
|
|
return true;
|
|
}
|
|
});
|
|
return found;
|
|
}
|
|
|
|
var knownTests = {};
|
|
function isTest(path, value) {
|
|
if (filter(path)) return false;
|
|
if (knownTests[path]) return knownTests[path];
|
|
|
|
if (!value)
|
|
value = tabManager.findTab(path).document.value;
|
|
|
|
test.runners.some(function(runner) {
|
|
if (runner.isTest(path, value)) {
|
|
knownTests[path] = runner;
|
|
return true;
|
|
}
|
|
return false;
|
|
});
|
|
|
|
return knownTests[path] || false;
|
|
}
|
|
|
|
// TODO export to ace editor and add loading detection
|
|
function scrollToDefinition(ace, line, lineEnd) {
|
|
var lineHeight = ace.renderer.$cursorLayer.config.lineHeight;
|
|
var lineVisibleStart = ace.renderer.scrollTop / lineHeight;
|
|
var linesVisible = ace.renderer.$size.height / lineHeight;
|
|
lineEnd = Math.min(lineEnd, line + linesVisible);
|
|
if (lineVisibleStart <= line && lineEnd <= lineVisibleStart + linesVisible)
|
|
return;
|
|
|
|
var SAFETY = 1.5;
|
|
ace.scrollToLine(Math.round((line + lineEnd) / 2 - SAFETY), true);
|
|
}
|
|
|
|
function openTestFile(nodes, onlyWhenOpen) {
|
|
(nodes || test.focussedPanel.tree.selectedNodes).forEach(function(n) {
|
|
var tab;
|
|
|
|
if (n.passed === 0) {
|
|
var found;
|
|
n.findAllNodes("test").some(function(n) {
|
|
found = n;
|
|
return n.passed === 0;
|
|
});
|
|
n = found;
|
|
}
|
|
|
|
if (n.type == "file" && (!n.ownPassed || !n.output)) {
|
|
if (onlyWhenOpen) {
|
|
tab = tabManager.findTab(n.path);
|
|
if (!tab || !tab.isActive())
|
|
return;
|
|
}
|
|
|
|
tabManager.openFile(n.path, true, function() {});
|
|
}
|
|
else if (n.type == "file" || n.pos) {
|
|
var fileNode = n.findFileNode();
|
|
if (onlyWhenOpen) {
|
|
tab = tabManager.findTab(fileNode.path);
|
|
if (!tab || !tab.isActive())
|
|
return;
|
|
}
|
|
|
|
tabManager.open({
|
|
path: fileNode.path,
|
|
active: true
|
|
}, function(err, tab) {
|
|
if (err) return console.error(err);
|
|
|
|
scrollTab(tab, n);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
function scrollTab(tab, n) {
|
|
var pos = n.selpos || n.pos;
|
|
var select = n.selpos ? {
|
|
row: n.selpos.el,
|
|
column: n.selpos.ec
|
|
} : undefined;
|
|
|
|
var ace = tab.editor.ace;
|
|
var scroll = function() {
|
|
ace.selection.clearSelection();
|
|
|
|
var sl = n.pos ? n.pos.sl : 0;
|
|
var el = n.pos ? n.pos.el : 0;
|
|
|
|
var a = n.annotations;
|
|
if (a && a.length) {
|
|
scrollToDefinition(ace, a[0].line, a[0].line);
|
|
ace.moveCursorTo(a[0].line - 1, a[0].column - 1);
|
|
}
|
|
else {
|
|
scrollToDefinition(ace, sl, el);
|
|
ace.moveCursorTo(pos ? pos.sl : 0, pos ? pos.sc : 0);
|
|
|
|
if (select)
|
|
ace.getSession().getSelection()
|
|
.selectToPosition({ row: pos.el, column: pos.ec });
|
|
}
|
|
};
|
|
|
|
if (!ace.session.doc.$lines.length)
|
|
ace.once("changeSession", scroll);
|
|
else if (!ace.renderer.$cursorLayer.config)
|
|
ace.once("afterRender", scroll);
|
|
else
|
|
scroll();
|
|
}
|
|
|
|
/***** Methods *****/
|
|
|
|
function run(nodes, options, callback) {
|
|
if (running) return stop(run.bind(this, nodes, options, callback));
|
|
|
|
running = true;
|
|
|
|
if (typeof nodes == "string") {
|
|
nodes = [findFileByPath(nodes)];
|
|
if (!nodes[0]) return callback(new Error("File not found"));
|
|
}
|
|
|
|
if (nodes && !Array.isArray(nodes))
|
|
callback = options, options = nodes, nodes = null;
|
|
|
|
if (typeof options == "function")
|
|
callback = options, options = null;
|
|
|
|
if (!nodes) {
|
|
nodes = tree.selectedNodes;
|
|
if (!nodes) return callback(new Error("Nothing to do"));
|
|
}
|
|
|
|
var withCodeCoverage = options && options.withCodeCoverage;
|
|
var transformRun = options && options.transformRun;
|
|
|
|
if (transformRun) {
|
|
var button = runGui.transformButton("stop");
|
|
button.setAttribute("command", "stoptest");
|
|
}
|
|
|
|
var list = [], found = {};
|
|
nodes.forEach(function(n) {
|
|
if (n.type == "prepare")
|
|
n = n.findFileNode(); // Weak solution. It should be able to run part of a test set without knowing tests
|
|
|
|
if (n.type == "all" || n.type == "root" || n.type == "runner")
|
|
n.findAllNodes("file").forEach(function(n) {
|
|
if (n.skip) return;
|
|
list.push(n);
|
|
found[n.path] = true;
|
|
});
|
|
else if (withCodeCoverage) {
|
|
var fileNode = n.findFileNode();
|
|
if (!found[fileNode.path])
|
|
list.push(fileNode);
|
|
}
|
|
else
|
|
list.push(n);
|
|
});
|
|
|
|
var firstRunner = list[0].findRunner();
|
|
var parallel = (!options || options.parallel === undefined
|
|
? test.config.parallel || firstRunner.defaultParallel
|
|
: options.parallel) || false;
|
|
var parallelConcurrency = (!options || options.parallelConcurrency === undefined
|
|
? test.config.parallelConcurrency || firstRunner.defaultParallelConcurrency
|
|
: options.parallelConcurrency) || 6;
|
|
|
|
options.parallel = parallel;
|
|
options.parallelConcurrency = parallelConcurrency;
|
|
|
|
test.lastTest = nodes;
|
|
|
|
var worker = function(node, callback) {
|
|
if (stopping) return callback(new Error("Terminated"));
|
|
|
|
if (node.status == "pending") { // TODO do this lazily
|
|
return populate(node, function(err) {
|
|
if (err) return callback(err);
|
|
_run(node, options, callback);
|
|
});
|
|
}
|
|
|
|
_run(node, options, callback);
|
|
};
|
|
var complete = function(err) {
|
|
emit("stop", { nodes: list });
|
|
callback(err, list);
|
|
};
|
|
|
|
if (parallel) {
|
|
var queue = async.queue(worker, parallelConcurrency);
|
|
queue.drain = complete;
|
|
list.forEach(function(item) {
|
|
queue.push(item, function() {});
|
|
});
|
|
}
|
|
else {
|
|
async.eachSeries(list, worker, complete);
|
|
}
|
|
}
|
|
|
|
var progress = {
|
|
log: function(node, chunk) {
|
|
node.fullOutput += chunk;
|
|
emit("log", chunk);
|
|
},
|
|
start: function(node) {
|
|
updateStatus(node, "running");
|
|
},
|
|
end: function(node) {
|
|
updateStatus(node, "loaded");
|
|
},
|
|
stop: []
|
|
};
|
|
|
|
function findTest(path) {
|
|
return (function recur(items) {
|
|
for (var j, i = 0; i < items.length; i++) {
|
|
j = items[i];
|
|
if (j.type == "file") {
|
|
if (j.path == path) return j;
|
|
}
|
|
else if (j.items) return recur(j.items);
|
|
}
|
|
})(rootNode.items);
|
|
}
|
|
|
|
function _run(node, options, callback) {
|
|
if (tree && tree.filterKeyword) {
|
|
if (node.type == "file")
|
|
node = findFileByPath(node.path);
|
|
else {
|
|
node.parent.findAllNodes(node.type).some(function(n) {
|
|
if (n.label == node.label) {
|
|
node = n;
|
|
return true;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
var runner = node.findRunner();
|
|
var fileNode = node.findFileNode();
|
|
|
|
if (!runner)
|
|
runner = findFileByPath(fileNode.path).findRunner();
|
|
|
|
if (runner.form)
|
|
options = runner.form.toJson(null, options || {});
|
|
|
|
fileNode.fullOutput = ""; // Reset output
|
|
updateStatus(node, "running");
|
|
|
|
// Clear previous run information
|
|
clear([node], true);
|
|
// emit("clearResult", { node: node });
|
|
|
|
var stop = runner.run(node, progress, options, function(err) {
|
|
updateStatus(node, "loaded");
|
|
|
|
var tab = tabManager.findTab(fileNode.path);
|
|
if (tab) decorate(fileNode, tab);
|
|
|
|
delete progress.stop[stopId];
|
|
|
|
callback(err, node);
|
|
|
|
// Write To Cache
|
|
writeToCache(runner, fileNode.path, fileNode.serialize());
|
|
|
|
emit("result", { node: node });
|
|
});
|
|
var stopId = progress.stop.push(stop) - 1;
|
|
}
|
|
|
|
function clearCache(runner, callback) {
|
|
fs.rmdir("~/.c9/cache/" + runner.name, { recursive: true }, function() {
|
|
callback && callback.apply(this, arguments);
|
|
});
|
|
}
|
|
|
|
function writeToCache(runner, path, cache, callback) {
|
|
fs.writeFile("~/.c9/cache/" + runner.name
|
|
+ "/" + path.replace(/\//g, "\\"), cache, function(err) {
|
|
callback && callback(err);
|
|
});
|
|
}
|
|
|
|
function refreshTree(node) {
|
|
while (node && !node.tree) node = node.parent;
|
|
var T = node && node.tree || tree;
|
|
if (T) T.refresh();
|
|
}
|
|
|
|
function updateStatus(node, s) {
|
|
// TODO make this more efficient by trusting the child nodes
|
|
if (node.type == "file" || node.type == "testset") {
|
|
var tests = node.findAllNodes("test|prepare");
|
|
var st, p = [];
|
|
tests.forEach(function(test) {
|
|
if (st === undefined && test.status != "loaded")
|
|
st = test.status;
|
|
if (!p[test.passed]) p[test.passed] = 0;
|
|
p[test.passed]++;
|
|
});
|
|
|
|
node.passed = p[3] ? 3 : (p[2] ? 2 : p[0] ? 0 : (p[1] ? 1 : undefined));
|
|
node.status = st || "loaded";
|
|
}
|
|
else if (node.type == "root") {
|
|
refreshTree(node);
|
|
return;
|
|
}
|
|
else {
|
|
node.status = s;
|
|
}
|
|
|
|
if (node.parent) updateStatus(node.parent, s);
|
|
else refreshTree(node);
|
|
}
|
|
|
|
function stop(callback) {
|
|
if (!running) return callback(new Error("Not Running"));
|
|
|
|
var timer;
|
|
stopping = Date.now();
|
|
plugin.once("stop", function(e) {
|
|
clearTimeout(timer);
|
|
|
|
(function _(items, first) {
|
|
items.forEach(function(node) {
|
|
if (node.items)
|
|
_(node.items);
|
|
else if (typeof node.passed != "number")
|
|
node.passed = 3;
|
|
|
|
// This caused unrun tests to no longer have an arrow
|
|
// if (first) updateStatus(node, "loaded");
|
|
});
|
|
})(e.nodes, true);
|
|
|
|
stopping = false;
|
|
callback();
|
|
});
|
|
|
|
progress.stop.forEach(function(stop) {
|
|
if (stop) stop();
|
|
});
|
|
|
|
timer = setTimeout(function() {
|
|
emit("stop", { nodes: []});
|
|
test.transformRunButton("run");
|
|
}, 5000);
|
|
}
|
|
|
|
function clear(nodes, onlyNodes) {
|
|
if (!nodes)
|
|
nodes = rootNode.items;
|
|
|
|
nodes.forEach(function(n) {
|
|
n.passed = undefined;
|
|
n.ownPassed = null;
|
|
n.output = "";
|
|
if (n.status == "running")
|
|
n.status = "loaded";
|
|
n.annotations = [];
|
|
if (n.items) clear(n.items, true);
|
|
});
|
|
|
|
if (onlyNodes) return;
|
|
|
|
if (tree.filterKeyword)
|
|
tree.filterKeyword = tree.filterKeyword;
|
|
else tree.refresh();
|
|
|
|
test.runners.forEach(function(runner) {
|
|
clearCache(runner);
|
|
});
|
|
|
|
clearAllDecorations();
|
|
}
|
|
|
|
function skip(nodes, callback) {
|
|
if (typeof nodes == "function")
|
|
callback = nodes, nodes = null;
|
|
|
|
if (!nodes) nodes = tree.selectedNodes;
|
|
|
|
var map = {};
|
|
nodes.forEach(function(fileNode) {
|
|
if (fileNode.type != "file") return;
|
|
|
|
if (!map[fileNode.path]) {
|
|
fileNode.skip = !fileNode.skip;
|
|
|
|
if (fileNode.skip)
|
|
test.config.skipped[fileNode.path] = true;
|
|
else
|
|
delete test.config.skipped[fileNode.path];
|
|
|
|
fileNode.findAllNodes("test").forEach(function(n) {
|
|
n.skip = fileNode.skip;
|
|
});
|
|
|
|
map[fileNode.path] = true;
|
|
}
|
|
});
|
|
|
|
test.saveConfig(function(err) {
|
|
tree.refresh();
|
|
callback(err);
|
|
});
|
|
}
|
|
|
|
function remove(nodes, callback) {
|
|
if (typeof nodes == "function")
|
|
callback = nodes, nodes = null;
|
|
|
|
if (!nodes) nodes = tree.selectedNodes;
|
|
|
|
nodes.forEach(function(fileNode) {
|
|
if (fileNode.type != "file") return;
|
|
|
|
if (!test.config.excluded[fileNode.path]) {
|
|
fileNode.parent.children.remove(fileNode);
|
|
fileNode.parent.items.remove(fileNode);
|
|
test.config.excluded[fileNode.path] = true;
|
|
}
|
|
});
|
|
|
|
test.saveConfig(function(err) {
|
|
tree.refresh();
|
|
callback(err);
|
|
});
|
|
}
|
|
|
|
// TODO: Think about moving this to a separate plugin
|
|
function decorate(fileNode, tab) {
|
|
var editor = tab.editor.ace;
|
|
var session = (tab.document.getSession() || 0).session;
|
|
|
|
if (!session || !tab.isActive()) {
|
|
tab.once("activate", function() {
|
|
setTimeout(function() { decorate(fileNode, tab); });
|
|
});
|
|
return;
|
|
}
|
|
if (!session.$testMarkers) {
|
|
session.$testMarkers = {};
|
|
session.on("changeEditor", function(e) {
|
|
if (e.oldEditor) {
|
|
// TODO cleanup
|
|
}
|
|
if (e.editor) {
|
|
decorateEditor(e.editor);
|
|
}
|
|
});
|
|
session.on("change", function(delta) {
|
|
var inlineWidgets = session.lineAnnotations;
|
|
var decorations = session.$decorations;
|
|
if (!inlineWidgets) return;
|
|
|
|
var startRow = delta.start.row;
|
|
var len = delta.end.row - startRow;
|
|
|
|
if (len === 0) {
|
|
if (inlineWidgets[startRow])
|
|
inlineWidgets[startRow] = undefined;
|
|
} else if (delta.action == 'remove') {
|
|
inlineWidgets.splice(startRow + 1, len);
|
|
decorations.splice(startRow + 1, len);
|
|
} else {
|
|
var args = new Array(len);
|
|
args.unshift(startRow, 0);
|
|
inlineWidgets.splice.apply(inlineWidgets, args);
|
|
decorations.splice.apply(decorations, args);
|
|
}
|
|
});
|
|
}
|
|
|
|
if (!session.widgetManager) {
|
|
session.widgetManager = new LineWidgets(session);
|
|
session.widgetManager.attach(editor);
|
|
}
|
|
|
|
clearDecoration(session);
|
|
|
|
var showInline = settings.getBool("user/test/@inlineresults");
|
|
|
|
var nodes = fileNode.findAllNodes("test|prepare");
|
|
if (fileNode.ownPassed) nodes.push(fileNode);
|
|
nodes.forEach(function(node) {
|
|
if (!node.parent) fileNode.fixParents();
|
|
|
|
if (node.passed !== undefined && (node.type == "test" || node.output)) {
|
|
var pos = node.pos ? node.pos.sl : 0;
|
|
session.addGutterDecoration(pos, "test-" + node.passed);
|
|
(session.$markers || (session.$markers = []))
|
|
.push([pos, "test-" + node.passed]);
|
|
}
|
|
if (showInline) {
|
|
if (node.annotations)
|
|
createStackWidget(editor, session, node);
|
|
if (node.output && node.output.trim())
|
|
createOutputWidget(editor, session, node);
|
|
}
|
|
});
|
|
}
|
|
|
|
function createOutputWidget(editor, session, node) {
|
|
// editor.session.unfold(pos.row);
|
|
// editor.selection.moveToPosition(pos);
|
|
|
|
var w = {
|
|
row: node.pos ? node.pos.el : 0,
|
|
fullWidth: true,
|
|
// coverGutter: true,
|
|
el: dom.createElement("div")
|
|
};
|
|
var extraClass = node.passed == 2
|
|
? "ace_error"
|
|
: (node.passed == 1 ? "ace_ok" : "ace_warning");
|
|
var el = w.el.appendChild(dom.createElement("div"));
|
|
var arrow = w.el.appendChild(dom.createElement("div"));
|
|
arrow.className = "error_widget_arrow " + extraClass;
|
|
|
|
var pos = node.pos
|
|
? { row: node.pos.el, column: node.pos.ec }
|
|
: { row: 0, column: 0 };
|
|
var left = editor.renderer.$cursorLayer.getPixelPosition(pos).left;
|
|
arrow.style.left = left /*+ editor.renderer.gutterWidth*/ - 5 + "px";
|
|
|
|
var runner = node.findRunner();
|
|
if (!runner)
|
|
runner = findFileByPath(node.findFileNode().path);
|
|
|
|
w.el.className = "error_widget_wrapper";
|
|
el.style.whiteSpace = "pre";
|
|
el.className = "error_widget " + extraClass;
|
|
el.innerHTML = runner
|
|
? runner.parseLinks(escapeHTML(node.output))
|
|
: escapeHTML(node.output);
|
|
|
|
var closeBtn = document.createElement("span");
|
|
closeBtn.textContent = "\xd7";
|
|
closeBtn.className = "widget-close-button";
|
|
w.el.appendChild(closeBtn);
|
|
closeBtn.onclick = function() { w.destroy(); };
|
|
closeBtn.onmousedown = function(e) { e.preventDefault(); };
|
|
|
|
w.el.addEventListener("click", function(e) {
|
|
if (e.target && e.target.className == "link") {
|
|
var link = e.target.getAttribute("link");
|
|
var parts = link.split(":");
|
|
fs.exists(parts[0], function(exists) {
|
|
if (!exists) {
|
|
commands.exec("navigate", null, {
|
|
keyword: link[0] == "/" ? link.substr(1) : link
|
|
});
|
|
return;
|
|
}
|
|
|
|
var path = parts[0].indexOf(c9.workspaceDir) === 0
|
|
? parts[0].replace(c9.workspaceDir, "")
|
|
: parts[0];
|
|
|
|
tabManager.open({
|
|
path: path,
|
|
focus: true,
|
|
document: {
|
|
ace: {
|
|
jump: {
|
|
row: Number(parts[1]),
|
|
column: Number(parts[1])
|
|
}
|
|
}
|
|
}
|
|
});
|
|
});
|
|
}
|
|
}, false);
|
|
w.el.addEventListener("contextmenu", function(e) {
|
|
if (e.which == 2 || e.which == 3) {
|
|
menuInlineContext.show(e.x + 1, e.y + 1);
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
return false;
|
|
}
|
|
}, false);
|
|
|
|
el.appendChild(dom.createElement("div"));
|
|
|
|
w.destroy = function() {
|
|
session.widgetManager.removeLineWidget(w);
|
|
w.destroyed = true;
|
|
};
|
|
|
|
session.widgetManager.addLineWidget(w);
|
|
session.$lineWidgets.push(w);
|
|
|
|
// w.el.onmousedown = editor.focus.bind(editor);
|
|
return w;
|
|
}
|
|
|
|
function decorateEditor(editor) {
|
|
if (editor.decorated)
|
|
return;
|
|
editor.renderer.on("afterRender", updateLines);
|
|
var onMouseDown = function(e) {
|
|
var widget = e.target;
|
|
if (widget.classList.contains("widget")) {
|
|
|
|
if (widget.annotation && widget.classList.contains("more")) {
|
|
if (widget.output && !widget.output.destroyed) {
|
|
widget.output.destroy();
|
|
widget.output = null;
|
|
}
|
|
else {
|
|
var a = widget.annotation;
|
|
widget.output = createOutputWidget(editor, a.session, {
|
|
pos: { el: a.row, ec: a.column },
|
|
passed: 0,
|
|
output: a.more,
|
|
findRunner: a.node.findRunner.bind(a.node)
|
|
});
|
|
}
|
|
}
|
|
e.stopPropagation();
|
|
}
|
|
};
|
|
editor.decorated = true;
|
|
editor.container.addEventListener("mousedown", onMouseDown, true);
|
|
}
|
|
|
|
function createStackWidget(editor, session, node) {
|
|
decorateEditor(editor);
|
|
var m, d;
|
|
node.annotations.forEach(function(item) {
|
|
m = item.message.trim();
|
|
if (m.length <= 50) d = m;
|
|
else {
|
|
if (m.match(/[\n\r]/) > -1)
|
|
d = m.split("\n")[0].substr(0, 45) + " ...";
|
|
else
|
|
d = m.substr(0, 20) + " ... " + m.substr(-25);
|
|
}
|
|
|
|
var pos = item.line - 1;
|
|
session.addGutterDecoration(pos, "test-0");
|
|
(session.$markers || (session.$markers = []))
|
|
.push([pos, "test-0"]);
|
|
|
|
session.lineAnnotations[pos] = {
|
|
display: d,
|
|
row: item.line - 1,
|
|
column: item.column,
|
|
more: m.length > 50 ? m : null,
|
|
session: session,
|
|
node: node
|
|
};
|
|
});
|
|
}
|
|
|
|
function updateLines(e, renderer) {
|
|
var textLayer = renderer.$textLayer;
|
|
var config = textLayer.config;
|
|
var session = textLayer.session;
|
|
|
|
if (!session.lineAnnotations) return;
|
|
|
|
var first = config.firstRow;
|
|
var last = config.lastRow;
|
|
|
|
var lineElements = textLayer.element.childNodes;
|
|
var lineElementsIdx = 0;
|
|
|
|
var row = first;
|
|
var foldLine = session.getNextFoldLine(row);
|
|
var foldStart = foldLine ? foldLine.start.row : Infinity;
|
|
|
|
var useGroups = textLayer.$useLineGroups();
|
|
|
|
while (true) {
|
|
if (row > foldStart) {
|
|
row = foldLine.end.row + 1;
|
|
foldLine = textLayer.session.getNextFoldLine(row, foldLine);
|
|
foldStart = foldLine ? foldLine.start.row : Infinity;
|
|
}
|
|
if (row > last)
|
|
break;
|
|
|
|
var lineElement = lineElements[lineElementsIdx++];
|
|
if (lineElement && session.lineAnnotations[row]) {
|
|
if (useGroups) lineElement = lineElement.lastChild;
|
|
var widget, a = session.lineAnnotations[row];
|
|
if (!a.element) {
|
|
widget = document.createElement("span");
|
|
widget.textContent = a.display;
|
|
widget.className = "widget stack-message" + (a.more ? " more" : "");
|
|
widget.annotation = a;
|
|
session.lineAnnotations[row].element = widget;
|
|
}
|
|
else widget = a.element;
|
|
|
|
lineElement.appendChild(widget);
|
|
}
|
|
row++;
|
|
}
|
|
}
|
|
|
|
function clearAllDecorations() {
|
|
tabManager.getTabs().forEach(function(tab) {
|
|
if (tab.editorType != "ace") return;
|
|
var session = (tab.document.getSession() || 0).session;
|
|
if (session) clearDecoration(session);
|
|
});
|
|
}
|
|
|
|
function clearDecoration(session) {
|
|
if (session.$markers) {
|
|
session.$decorations.forEach(function(m, i) {
|
|
if (m)
|
|
session.$decorations[i] = m.replace(/ test-[01234]/g, "");
|
|
});
|
|
}
|
|
if (session.lineAnnotations) {
|
|
session.lineAnnotations.forEach(function(item) {
|
|
if (item && item.element && item.element.parentNode)
|
|
item.element.remove();
|
|
});
|
|
}
|
|
if (session.$lineWidgets) {
|
|
session.$lineWidgets.forEach(function(widget) {
|
|
session.widgetManager.removeLineWidget(widget);
|
|
});
|
|
}
|
|
session.$markers = [];
|
|
session.lineAnnotations = [];
|
|
session.$lineWidgets = [];
|
|
}
|
|
|
|
function refresh() {
|
|
tree && tree.refresh();
|
|
}
|
|
|
|
/***** Lifecycle *****/
|
|
|
|
plugin.on("load", function() {
|
|
load();
|
|
});
|
|
plugin.on("draw", function(e) {
|
|
draw(e);
|
|
});
|
|
plugin.on("show", function(e) {
|
|
// txtFilter.focus();
|
|
// txtFilter.select();
|
|
});
|
|
plugin.on("hide", function(e) {
|
|
// Cancel Preview
|
|
// tabs.preview({ cancel: true });
|
|
});
|
|
plugin.on("unload", function() {
|
|
drawn = false;
|
|
tree = null;
|
|
stopping = null;
|
|
menuContext = null;
|
|
running = null;
|
|
boxFilter = null;
|
|
menuInlineContext = null;
|
|
});
|
|
|
|
/***** Register and define API *****/
|
|
|
|
/**
|
|
* @class Template
|
|
* @extends Plugin
|
|
* @singleton
|
|
*/
|
|
plugin.freezePublicAPI({
|
|
/**
|
|
* @property {Object} The tree implementation
|
|
* @private
|
|
*/
|
|
get tree() { return tree; },
|
|
|
|
/**
|
|
*
|
|
*/
|
|
get contextMenu() { return menuContext; },
|
|
|
|
/**
|
|
*
|
|
*/
|
|
get root() { return rootNode; },
|
|
|
|
/**
|
|
*
|
|
*/
|
|
refresh: refresh,
|
|
|
|
/**
|
|
*
|
|
*/
|
|
run: run,
|
|
|
|
/**
|
|
*
|
|
*/
|
|
stop: stop,
|
|
|
|
/**
|
|
*
|
|
*/
|
|
skip: skip,
|
|
|
|
/**
|
|
*
|
|
*/
|
|
remove: remove,
|
|
|
|
/**
|
|
*
|
|
*/
|
|
openTestFile: openTestFile,
|
|
|
|
/**
|
|
*
|
|
*/
|
|
findTest: findTest,
|
|
|
|
/**
|
|
*
|
|
*/
|
|
findFileByPath: findFileByPath,
|
|
|
|
/**
|
|
*
|
|
*/
|
|
writeToCache: writeToCache,
|
|
|
|
/**
|
|
*
|
|
*/
|
|
clearCache: clearCache
|
|
});
|
|
|
|
register(null, {
|
|
"test.all": plugin
|
|
});
|
|
}
|
|
}); |