kopia lustrzana https://github.com/c9/core
610 wiersze
23 KiB
JavaScript
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
|
|
});
|
|
}
|
|
}); |