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

610 wiersze
23 KiB
JavaScript

define(function(require, exports, module) {
main.consumes = [
"TestRunner", "proc", "util", "fs", "test", "language", "c9", "debugger"
];
main.provides = ["test.mocha"];
return main;
function main(options, imports, register) {
var TestRunner = imports.TestRunner;
var proc = imports.proc;
var util = imports.util;
var test = imports.test;
var fs = imports.fs;
var c9 = imports.c9;
var language = imports.language;
var debug = imports["debugger"];
var Coverage = test.Coverage;
var dirname = require("path").dirname;
var basename = require("path").basename;
var join = require("path").join;
/***** Initialization *****/
var plugin = new TestRunner("Ajax.org", main.consumes, {
caption: "Mocha Javascript Tests",
query: {
id: "mocha",
label: "Mocha Test Runner",
def: {
match: {
content: ["^\\s*describe\\s*\\(", "^\\s*suite\\s*\\("],
filename: [".js$"]
},
exclude: {
dir: ["node_modules"],
file: []
},
search: "*"
}
},
getName: function(name) {
return name.substr(1);
},
options: [
{
title: "Enable Debugger",
type: "checkbox-single",
setting: "state/test/mocha/@debug",
name: "debug"
}
]
});
// var emit = plugin.getEmitter();
var currentPty = [];
var isWin = c9.platform == "win32";
var debugging;
var uniqueId = 0;
function load() {
if (test.inactive)
return;
}
/***** Methods *****/
function populate(node, callback) {
fs.readFile(node.path, function(err, contents) {
if (err) return callback(err);
updateOutline(node, contents, callback);
});
}
var wid = 0;
function updateOutline(node, contents, callback) {
language.getWorker(function(err, worker) {
if (err) return callback && callback(err) || console.error(err);
var currentId = ++wid;
worker.emit("mocha_outline", { data: { id: currentId, code: contents }});
worker.on("mocha_outline_result", function onResponse(e) {
if (e.data.id !== currentId) return;
worker.off("mocha_outline_result", onResponse);
node.importItems(e.data.result);
callback && callback();
});
});
}
function getTestNode(node, id, name) {
var count = 0;
var found = (function recur(items, pname) {
for (var j, i = 0; i < items.length; i++) {
j = items[i];
if (j.type == "test") count++;
if (id !== undefined ? count == id : pname + j.label == name)
return j;
if (j.items) {
var found = recur(j.items,
pname + (j.type == "testset" ? j.label + " " : ""));
if (found) return found;
}
}
})([node], "");
// TODO optional fallback to using id
return found;
}
function findTestName(name, node) {
var nodes = node.findAllNodes("testset");
for (var i = 0; i < nodes.length; i++) {
var lbl = nodes[i].label.trim();
var idx = name.indexOf(lbl);
if (~idx)
return findTestName(name.substr(idx + lbl.length).trim(), nodes[i]);
}
return [name, node];
}
function getFullTestName(node) {
var name = [];
do {
name.unshift(node.label);
node = node.parent;
} while (node.type != "file");
return name.join(" ");
}
function run(node, progress, options, callback) {
if (typeof options == "function")
callback = options, options = null;
var fileNode;
var exec = "mocha", args = ["--reporter", "tap"];
var allTests = node.findAllNodes("test");
var allTestIndex = 0;
if (node.type == "file") {
fileNode = node;
progress.start(allTests[allTestIndex] || node);
}
else {
fileNode = node.findFileNode();
progress.start(node.type == "test" ? node : allTests[allTestIndex]);
args.push("--grep", util.escapeRegExp(getFullTestName(node)) // "^" +
+ (node.type == "test" ? "$" : ""));
}
fileNode.ownPassed = null;
fileNode.output = "";
var withCodeCoverage = options && options.withCodeCoverage;
var withDebug = options && options.debug;
var parallel = options && options.parallel;
if (!withCodeCoverage && !parallel && withDebug) {
// "${debug?--nocrankshaft}",
// "${debug?--nolazy}",
// "${debug?`node --version | grep -vqE \"v0\\..\\.\" && echo --nodead_code_elimination`}",
// "${debug?--debug-brk=15454}",
args.push("--debug", "--debug-brk"); // TODO extra node options
debugging = true;
debug.debug({
STARTED: 2,
runner: {
"debugger": "v8",
"debugport": 5858,
"disabled": {
liveUpdate: true
}
},
running: 2,
name: plugin.root.label,
meta: { $debugger: true }
}, false, function(err) {
if (err)
return (debugging = false); // Either the debugger is not found or paused
});
}
var path = join(c9.workspaceDir, fileNode.path);
args.push(path);
var coveragePath = "~/.c9/coverage/run" + (++uniqueId);
if (withCodeCoverage) {
exec = "istanbul";
args.unshift("cover", "--print", "none", "--report",
"lcovonly", "--dir", coveragePath,
isWin ? "node_modules/mocha/bin/_mocha" : "_mocha", "--");
}
if (isWin) {
args.unshift("-c", '"$0" "$@"', exec);
exec = "bash.exe";
}
else {
args.unshift("-l", "-c", '"$0" "$@"', exec); // force a login shell
exec = "bash";
}
proc.pty(exec, {
args: args,
cwd: dirname(path),
fakePty: isWin
}, function(err, pty) {
if (err) return callback(err);
var buffer = createBuffer(pty, fileNode, node, progress,
allTests, allTestIndex, withCodeCoverage,
coveragePath, callback);
pty.on("data", function(c) {
buffer.read(c);
});
pty.on("exit", function(c) {
buffer.end(c);
});
});
return stop;
}
function createBuffer(pty, fileNode, node, progress, allTests,
allTestIndex, withCodeCoverage, coveragePath, callback) {
var ptyId = currentPty.push(pty);
var lastResultNode, testCount, bailed;
var output = "", totalTests = 0;
return {
buffer: "",
incomplete: "",
read: function(c) {
// Log to the raw viewer
progress.log(fileNode, c);
var lines = (this.incomplete + c).split(/[\r\n]+/);
this.incomplete = "";
for (var line, i = 0, l = lines.length; i < l; i++) {
line = lines[i];
// The last line is always incomplete
if (i == l - 1) {
this.incomplete += line;
}
// Number of tests
else if (line.match(/^(\d+)\.\.(\d+)$/m)) {
testCount = parseInt(RegExp.$2, 10);
}
// Final Statements
else if (line.match(/^# (?:tests|pass|fail)/)) {
continue;
}
// Bail
else if (line.match(/^Bail out!(.*)$/m)) {
bailed = 3; // RegExp.$1;
}
// Update parsed nodes (set, test)
else if (line.match(/^(ok|not ok)\s+(\d+)\s+(.*)$/m)) {
this.readTest(this.buffer);
this.buffer = line;
}
else {
this.buffer += (this.buffer ? "\n" : "") + line;
}
}
},
readTest: function(c) {
// Update parsed nodes (set, test)
var m = c.match(/^(ok|not ok)\s+(\d+)\s+(.*)$/m);
if (m) {
var pass = m[1] == "ok" ? 1 : 0;
var id = m[2];
var name = m[3];
if (name.match(/"(before all|before each|after all|after each)" hook/, "$1")) {
name = name.replace(/"(before all|before each|after all|after each)" hook .*/, "$1");
id = undefined;
if (!pass) bailed = 2, pass = 2;
}
// TODO when having multiple before/after* find out the
// right one based on the line number in the stacktrace
// Update Node
var resultNode = (node.type == "test"
? node
: getTestNode(node, id, name));
// I suspect this is no longer needed
if (!resultNode) {
var context = findTestName(name, node);
resultNode = fileNode.addTest({
label: context[0],
type: "test",
kind: "it",
pos: { sl: 0, el: 0, sc: 0, ec: 0 }
}, context[1]);
}
// if (!resultNode)
// resultNode = lastResultNode
// || node.findAllNodes("test")[0];
// if (!resultNode)
// return (bailed = 2); // TODO test this
lastResultNode = resultNode;
// Set Results
resultNode.output = output + "\n";
resultNode.passed = pass;
resultNode.annotations = null;
// Reset output
output = "";
// Count the tests
totalTests++;
// Update progress
progress.end(resultNode);
c = c.replace(/^.*([\r\n]+|$)/, "");
if (!bailed) {
var nextTest = allTests[++allTestIndex]; // findNextTest(resultNode);
if (nextTest) progress.start(nextTest);
}
}
// Output
var stackTrace;
// Detect stack trace or timeout
if (c.match(/^\s*Error: timeout/) || c.match(/^\s+at .*:\d+:\d+\)?$/m)) {
if (!lastResultNode) {
lastResultNode = fileNode; // getTestNode(fileNode, 1);
fileNode.ownPassed = 2;
fileNode.output = c;
return;
}
if (c.match(/^\s*Error: timeout/)) {
lastResultNode.output += c;
}
else {
stackTrace = parseTrace(c);
if (stackTrace) {
if (!withCodeCoverage) {
if (!lastResultNode.annotations)
lastResultNode.annotations = [];
var path = join(c9.workspaceDir, fileNode.path);
var pos = stackTrace.findPath(path);
if (!pos)
output += c;
else {
lastResultNode.annotations.push({
line: pos.lineNumber,
column: pos.column,
message: c.trim().replace(/^\s+at/mg, " at") // stackTrace.message
});
}
}
else
lastResultNode.output += c;
}
}
return;
}
output += c;
},
end: function(c) {
delete currentPty[ptyId];
if (this.buffer)
this.readTest(this.buffer);
if (output) {
if (lastResultNode)
lastResultNode.output += output;
else
fileNode.output = output;
output = "";
}
// Special Case for Syntax Errors
if (fileNode.output.indexOf("SyntaxError:") > -1) {
var stackTrace = parseTrace(fileNode.output);
var filepath = isWin ? fileNode.path.replace(/\//g, "\\") : fileNode.path;
var rePath = new RegExp(util.escapeRegExp(filepath) + ":(\\d+)");
var m = fileNode.output.match(rePath);
if (m[1]) {
if (!fileNode.annotations)
fileNode.annotations = [];
var lineNumber = parseInt(m[1]);
fileNode.annotations.push({
line: lineNumber,
column: 0,
message: "SyntaxError:" + stackTrace.message.split("SyntaxError:")[1]
});
fileNode.output = stackTrace.message + "\n"
+ fileNode.path.substr(1) + ":" + lineNumber;
fileNode.ownPassed = 3;
}
}
if (testCount !== totalTests) {
if (!pty.isKilled)
fileNode.ownPassed = 2;
}
else if (bailed || pty.isKilled)
fileNode.ownPassed = pty.isKilled ? 3 : bailed;
if (withCodeCoverage && !bailed) {
fs.readFile(coveragePath + "/lcov.info", function(err, lcovString) {
if (err) return done(err);
node.coverage = Coverage.fromLCOV(lcovString, coveragePath);
done();
});
}
else {
node.coverage = null;
done();
}
function done(err) {
// Cleanup for before/after failure
allTests.forEach(function(n) {
if (n.status != "loaded")
progress.end(n);
});
callback(err, node);
}
}
};
}
/**
* This parses the different stack traces and puts them into one format
* This borrows heavily from TraceKit (https://github.com/occ/TraceKit)
* From: https://github.com/errwischt/stacktrace-parser/blob/master/lib/stacktrace-parser.js
*/
var UNKNOWN_FUNCTION = '<unknown>';
function parseTrace(stackString) {
var node = /^\s*at (?:((?:\[object object\])?\S+(?: \[as \S+\])?) )?\(?(.*?):(\d+)(?::(\d+))?\)?\s*$/i;
var lines = stackString.split('\n');
var stack = [];
var message = [];
var parts, started;
for (var i = 0, j = lines.length; i < j; ++i) {
if ((parts = node.exec(lines[i]))) {
stack.push({
'file': parts[2],
'methodName': parts[1] || UNKNOWN_FUNCTION,
'lineNumber': +parts[3],
'column': parts[4] ? +parts[4] : null
});
started = true;
}
else if (!started) {
message.push(lines[i]);
}
}
stack.message = message.join("\n");
if ((stack.message + stack[0].file).indexOf("mocha/lib/runner.js") > -1)
return false;
stack.findPath = function(path, isFilename) {
for (var i = 0; i < stack.length; i++) {
if (stack[i].file == path)
return stack[i];
}
return isFilename ? false : this.findPath(basename(path), true);
};
return stack;
}
function stop() {
currentPty.forEach(function(pty) {
pty.isKilled = true;
pty.kill();
});
currentPty = [];
if (debugging)
debug.stop();
}
var reStack = /([\\\/\w-_\.]+):(\d+)(?::(\d+))?/g;
function parseLinks(strOutput) {
return strOutput.replace(reStack, function(m, name, l, c) {
name = name.replace(c9.workspaceDir, "");
if (name.charAt(0) != "/") name = "/" + name;
var link = name + ":" + (l - 1) + (c ? ":" + c : "");
return "<span class='link' link='" + link + "'>" + m + "</span>";
});
}
function findFileByPath(path) {
var found = false;
plugin.root.findAllNodes("file").some(function(n) {
if (n.path == path) {
found = n;
return true;
}
});
return found;
}
function fileChange(options) {
// Update file
var fileNode = findFileByPath(options.path);
if (!fileNode)
fileNode = plugin.createFile(options.path.substr(1));
if (!fileNode.runner)
fileNode.runner = plugin;
if (fileNode.items.length || options.runonsave)
updateOutline(fileNode, options.value, function() {
options.refresh();
options.run(fileNode); // Run file
});
}
/***** Lifecycle *****/
plugin.on("init", function() {
});
plugin.on("load", function() {
// Hook into the language worker
language.registerLanguageHandler("plugins/c9.ide.test.mocha/mocha_outline_worker");
load();
});
plugin.on("unload", function() {
currentPty = null;
debugging = null;
});
/***** Register and define API *****/
plugin.freezePublicAPI({
/**
*
*/
populate: populate,
/**
*
*/
parseLinks: parseLinks,
/**
*
*/
fileChange: fileChange,
/**
*
*/
run: run,
/**
*
*/
stop: stop
});
register(null, {
"test.mocha": plugin
});
}
});