1
0
mirror of https://github.com/node-red/node-red.git synced 2023-10-10 13:36:53 +02:00

Extract all core runtime messages

This commit is contained in:
Nick O'Leary 2015-05-08 14:21:01 +01:00
parent aa18c65fa8
commit 6249083431
13 changed files with 123 additions and 64 deletions

View File

@ -6,8 +6,8 @@
"settings": "Settings file : __path__" "settings": "Settings file : __path__"
} }
}, },
"nodes": { "server": {
"loading": "Loading palette nodes", "loading": "Loading palette nodes",
"errors": "Failed to register __count__ node type", "errors": "Failed to register __count__ node type",
"errors_plural": "Failed to register __count__ node types", "errors_plural": "Failed to register __count__ node types",
@ -28,10 +28,64 @@
"uninstall-failed": "Uninstall failed", "uninstall-failed": "Uninstall failed",
"uninstall-failed-long": "Uninstall of module __name__ failed:", "uninstall-failed-long": "Uninstall of module __name__ failed:",
"uninstalled": "Uninstalled module: __name__" "uninstalled": "Uninstalled module: __name__"
}
},
"api": {
"flows": {
"error-save": "Error saving flows: __message__"
},
"library": {
"error-load-entry": "Error loading library entry '__path__': __message__",
"error-save-entry": "Error saving library entry '__path__': __message__",
"error-load-flow": "Error loading flow '__path__': __message__",
"error-save-flow": "Error saving flow '__path__': __message__"
},
"nodes": {
"enabled": "Enabled node types:",
"disabled": "Disabled node types:",
"error-enable": "Failed to enable node:"
}
},
"comms": {
"error": "Communication channel error: __message__",
"error-server": "Communication server error: __message__",
"error-send": "Communication send error: __message__"
},
"nodes": {
"credentials": {
"error":"Error loading credentials: __message__",
"not-registered": "Credential type '__type__' is not registered"
},
"flows": {
"registered-missing": "Missing type registered: __type__",
"error": "Error loading flows: __message__",
"starting-modified-nodes": "Starting modified nodes",
"starting-modified-flows": "Starting modified flows",
"starting-flows": "Starting flows",
"started-modified-nodes": "Started modified nodes",
"started-modified-flows": "Started modified flows",
"started-flows": "Started flows",
"stopping-modified-nodes": "Stopping modified nodes",
"stopping-modified-flows": "Stopping modified flows",
"stopping-flows": "Stopping flows",
"stopped-modified-nodes": "Stopped modified nodes",
"stopped-modified-flows": "Stopped modified flows",
"stopped-flows": "Stopped flows",
"stopped": "Stopped",
"missing-types": "Waiting for missing types to be registered:"
} }
} },
"storage": {
"localfilesystem": {
"user-dir": "User directory : __path__",
"flows-file": "Flows file : __path__",
"create": "Creating new flow file"
}
}
} }

View File

@ -15,6 +15,7 @@
**/ **/
var log = require("../log"); var log = require("../log");
var redNodes = require("../nodes"); var redNodes = require("../nodes");
var settings = require("../settings"); var settings = require("../settings");
@ -30,7 +31,7 @@ module.exports = {
redNodes.setFlows(flows,deploymentType).then(function() { redNodes.setFlows(flows,deploymentType).then(function() {
res.send(204); res.send(204);
}).otherwise(function(err) { }).otherwise(function(err) {
log.warn("Error saving flows : "+err.message); log.warn(log._("api.flows.error-save",{message:err.message}));
log.warn(err.stack); log.warn(err.stack);
res.json(500,{error:"unexpected_error", message:err.message}); res.json(500,{error:"unexpected_error", message:err.message});
}); });

View File

@ -17,6 +17,7 @@
var redApp = null; var redApp = null;
var storage = require("../storage"); var storage = require("../storage");
var log = require("../log"); var log = require("../log");
var needsPermission = require("./auth").needsPermission; var needsPermission = require("./auth").needsPermission;
function createLibrary(type) { function createLibrary(type) {
@ -34,7 +35,7 @@ function createLibrary(type) {
} }
}).otherwise(function(err) { }).otherwise(function(err) {
if (err) { if (err) {
log.warn("Error loading library entry '"+path+"' : "+err); log.warn(log._("api.library.error-load-entry",{path:path,message:err}));
if (err.message.indexOf('forbidden') === 0) { if (err.message.indexOf('forbidden') === 0) {
log.audit({event: "library.get",type:type,error:"forbidden"},req); log.audit({event: "library.get",type:type,error:"forbidden"},req);
res.send(403); res.send(403);
@ -56,7 +57,7 @@ function createLibrary(type) {
log.audit({event: "library.set",type:type},req); log.audit({event: "library.set",type:type},req);
res.send(204); res.send(204);
}).otherwise(function(err) { }).otherwise(function(err) {
log.warn("Error saving library entry '"+path+"' : "+err); log.warn(log._("api.library.error-save-entry",{path:path,message:err}));
if (err.message.indexOf('forbidden') === 0) { if (err.message.indexOf('forbidden') === 0) {
log.audit({event: "library.set",type:type,error:"forbidden"},req); log.audit({event: "library.set",type:type,error:"forbidden"},req);
res.send(403); res.send(403);
@ -88,7 +89,7 @@ module.exports = {
res.send(data); res.send(data);
}).otherwise(function(err) { }).otherwise(function(err) {
if (err) { if (err) {
log.warn("Error loading flow '"+req.params[0]+"' : "+err); log.warn(log._("api.library.error-load-flow",{path:req.params[0],message:err}));
if (err.message.indexOf('forbidden') === 0) { if (err.message.indexOf('forbidden') === 0) {
log.audit({event: "library.get",type:"flow",path:req.params[0],error:"forbidden"},req); log.audit({event: "library.get",type:"flow",path:req.params[0],error:"forbidden"},req);
res.send(403); res.send(403);
@ -105,7 +106,7 @@ module.exports = {
log.audit({event: "library.set",type:"flow",path:req.params[0]},req); log.audit({event: "library.set",type:"flow",path:req.params[0]},req);
res.send(204); res.send(204);
}).otherwise(function(err) { }).otherwise(function(err) {
log.warn("Error loading flow '"+req.params[0]+"' : "+err); log.warn(log._("api.library.error-save-flow",{path:req.params[0],message:err}));
if (err.message.indexOf('forbidden') === 0) { if (err.message.indexOf('forbidden') === 0) {
log.audit({event: "library.set",type:"flow",path:req.params[0],error:"forbidden"},req); log.audit({event: "library.set",type:"flow",path:req.params[0],error:"forbidden"},req);
res.send(403); res.send(403);

View File

@ -226,12 +226,12 @@ function putNode(node, enabled) {
return promise.then(function(info) { return promise.then(function(info) {
if (info.enabled === enabled && !info.err) { if (info.enabled === enabled && !info.err) {
comms.publish("node/"+(enabled?"enabled":"disabled"),info,false); comms.publish("node/"+(enabled?"enabled":"disabled"),info,false);
log.info(" "+(enabled?"Enabled":"Disabled")+" node types:"); log.info(" "+log._("api.nodes."+(enabled?"enabled":"disabled")));
for (var i=0;i<info.types.length;i++) { for (var i=0;i<info.types.length;i++) {
log.info(" - "+info.types[i]); log.info(" - "+info.types[i]);
} }
} else if (enabled && info.err) { } else if (enabled && info.err) {
log.warn("Failed to enable node:"); log.warn(log._("api.nodes.error-enable"));
log.warn(" - "+info.name+" : "+info.err); log.warn(" - "+info.name+" : "+info.err);
} }
return info; return info;

View File

@ -65,7 +65,7 @@ function start() {
try { try {
msg = JSON.parse(data); msg = JSON.parse(data);
} catch(err) { } catch(err) {
log.warn("comms received malformed message : "+err.toString()); log.trace("comms received malformed message : "+err.toString());
return; return;
} }
if (!pendingAuth) { if (!pendingAuth) {
@ -119,12 +119,12 @@ function start() {
} }
}); });
ws.on('error', function(err) { ws.on('error', function(err) {
log.warn("comms error : "+err.toString()); log.warn(log._("comms.error",{message:err.toString()}));
}); });
}); });
wsServer.on('error', function(err) { wsServer.on('error', function(err) {
log.warn("comms server error : "+err.toString()); log.warn(log._("comms.error-server",{message:err.toString()}));
}); });
lastSentTime = Date.now(); lastSentTime = Date.now();
@ -167,7 +167,7 @@ function publishTo(ws,topic,data) {
try { try {
ws.send(msg); ws.send(msg);
} catch(err) { } catch(err) {
log.warn("comms send error : "+err.toString()); log.warn(log._("comms.error-send",{message:err.toString()}));
} }
} }

View File

@ -17,6 +17,8 @@
var util = require("util"); var util = require("util");
var EventEmitter = require("events").EventEmitter; var EventEmitter = require("events").EventEmitter;
var i18n = require("./i18n");
var levels = { var levels = {
off: 1, off: 1,
fatal: 10, fatal: 10,
@ -143,3 +145,5 @@ var log = module.exports = {
log.log(msg); log.log(msg);
} }
} }
log["_"] = i18n._;

View File

@ -17,6 +17,7 @@
var when = require("when"); var when = require("when");
var log = require("../log"); var log = require("../log");
var needsPermission = require("../api/auth").needsPermission; var needsPermission = require("../api/auth").needsPermission;
var credentialCache = {}; var credentialCache = {};
@ -75,7 +76,7 @@ module.exports = {
return storage.getCredentials().then(function (creds) { return storage.getCredentials().then(function (creds) {
credentialCache = creds; credentialCache = creds;
}).otherwise(function (err) { }).otherwise(function (err) {
log.warn("Error loading credentials : " + err); log.warn(log._("nodes.credentials.error",{message: err}));
}); });
}, },
@ -167,7 +168,7 @@ module.exports = {
var dashedType = nodeType.replace(/\s+/g, '-'); var dashedType = nodeType.replace(/\s+/g, '-');
var definition = credentialsDef[dashedType]; var definition = credentialsDef[dashedType];
if (!definition) { if (!definition) {
log.warn('Credential Type ' + nodeType + ' is not registered.'); log.warn(log._("nodes.credentials.not-registered",{type:nodeType}));
return; return;
} }

View File

@ -21,6 +21,7 @@ var typeRegistry = require("./registry");
var credentials = require("./credentials"); var credentials = require("./credentials");
var Flow = require("./Flow"); var Flow = require("./Flow");
var log = require("../log"); var log = require("../log");
var events = require("../events"); var events = require("../events");
var redUtil = require("../util"); var redUtil = require("../util");
var storage = null; var storage = null;
@ -36,7 +37,7 @@ var activeConfigNodes = {};
events.on('type-registered',function(type) { events.on('type-registered',function(type) {
if (activeFlow && activeFlow.typeRegistered(type)) { if (activeFlow && activeFlow.typeRegistered(type)) {
log.info("Missing type registered: "+type); log.info(log._("nodes.flows.registered-missing", {type:type}));
} }
}); });
@ -57,7 +58,7 @@ var flowNodes = module.exports = {
flowNodes.startFlows(); flowNodes.startFlows();
}); });
}).otherwise(function(err) { }).otherwise(function(err) {
log.warn("Error loading flows : "+err); log.warn(log._("nodes.flows.error",{err:err}));
console.log(err.stack); console.log(err.stack);
}); });
}, },
@ -132,21 +133,21 @@ var flowNodes = module.exports = {
}, },
startFlows: function(configDiff) { startFlows: function(configDiff) {
if (configDiff) { if (configDiff) {
log.info("Starting modified "+configDiff.type); log.info(log._("nodes.flows.starting-modified-"+configDiff.type));
} else { } else {
log.info("Starting flows"); log.info(log._("nodes.flows.starting-flows"));
} }
try { try {
activeFlow.start(configDiff); activeFlow.start(configDiff);
if (configDiff) { if (configDiff) {
log.info("Started modified "+configDiff.type); log.info(log._("nodes.flows.started-modified-"+configDiff.type));
} else { } else {
log.info("Started flows"); log.info(log._("nodes.flows.started-flows"));
} }
} catch(err) { } catch(err) {
var missingTypes = activeFlow.getMissingTypes(); var missingTypes = activeFlow.getMissingTypes();
if (missingTypes.length > 0) { if (missingTypes.length > 0) {
log.info("Waiting for missing types to be registered:"); log.info(log._("nodes.flows.missing-types"));
var knownUnknowns = 0; var knownUnknowns = 0;
for (var i=0;i<missingTypes.length;i++) { for (var i=0;i<missingTypes.length;i++) {
var type = missingTypes[i]; var type = missingTypes[i];
@ -169,21 +170,21 @@ var flowNodes = module.exports = {
}, },
stopFlows: function(configDiff) { stopFlows: function(configDiff) {
if (configDiff) { if (configDiff) {
log.info("Stopping modified "+configDiff.type); log.info(log._("nodes.flows.stopping-modified-"+configDiff.type));
} else { } else {
log.info("Stopping flows"); log.info(log._("nodes.flows.stopping-flows"));
} }
if (activeFlow) { if (activeFlow) {
return activeFlow.stop(configDiff).then(function() { return activeFlow.stop(configDiff).then(function() {
if (configDiff) { if (configDiff) {
log.info("Stopped modified "+configDiff.type); log.info(log._("nodes.flows.stopped-modified-"+configDiff.type));
} else { } else {
log.info("Stopped flows"); log.info(log._("nodes.flows.stopped-flows"));
} }
return; return;
}); });
} else { } else {
log.info("Stopped"); log.info(log._("nodes.flows.stopped"));
return; return;
} }
}, },

View File

@ -251,7 +251,7 @@ function loadNodeSet(node) {
red["_"] = function() { red["_"] = function() {
var args = Array.prototype.slice.call(arguments, 0); var args = Array.prototype.slice.call(arguments, 0);
args[0] = node.namespace+":"+args[0]; args[0] = node.namespace+":"+args[0];
return red.i18n._.apply(null,args); return i18n._.apply(null,args);
} }
var promise = r(red); var promise = r(red);
if (promise != null && typeof promise.then === "function") { if (promise != null && typeof promise.then === "function") {

View File

@ -57,7 +57,6 @@ var RED = {
credentials: credentials, credentials: credentials,
events: events, events: events,
log: log, log: log,
i18n: i18n,
comms: comms, comms: comms,
settings:settings, settings:settings,
util: util, util: util,
@ -78,6 +77,4 @@ var RED = {
get server() { return server.server } get server() { return server.server }
}; };
//RED["_"] = i18n._;
module.exports = RED; module.exports = RED;

View File

@ -58,12 +58,12 @@ function start() {
reportMetrics(); reportMetrics();
}, settings.runtimeMetricInterval||15000); }, settings.runtimeMetricInterval||15000);
} }
console.log("\n\n"+i18n._("runtime.welcome")+"\n===================\n"); console.log("\n\n"+log._("runtime.welcome")+"\n===================\n");
if (settings.version) { if (settings.version) {
log.info(i18n._("runtime.version",{component:"Node-RED",version:"v"+settings.version})); log.info(log._("runtime.version",{component:"Node-RED",version:"v"+settings.version}));
} }
log.info(i18n._("runtime.version",{component:"Node.js ",version:process.version})); log.info(log._("runtime.version",{component:"Node.js ",version:process.version}));
log.info(i18n._("nodes.loading")); log.info(log._("server.loading"));
redNodes.init(settings,storage,app); redNodes.init(settings,storage,app);
return redNodes.load().then(function() { return redNodes.load().then(function() {
@ -77,13 +77,13 @@ function start() {
log.warn("["+nodeErrors[i].name+"] "+nodeErrors[i].err); log.warn("["+nodeErrors[i].name+"] "+nodeErrors[i].err);
} }
} else { } else {
log.warn(i18n._("nodes.errors",{count:nodeErrors.length})); log.warn(log._("server.errors",{count:nodeErrors.length}));
log.warn(i18n._("nodes.errors-help")); log.warn(log._("server.errors-help"));
} }
log.warn("------------------------------------------"); log.warn("------------------------------------------");
} }
if (nodeMissing.length > 0) { if (nodeMissing.length > 0) {
log.warn(i18n._("nodes.missing-modules")); log.warn(log._("server.missing-modules"));
var missingModules = {}; var missingModules = {};
for (i=0;i<nodeMissing.length;i++) { for (i=0;i<nodeMissing.length;i++) {
var missing = nodeMissing[i]; var missing = nodeMissing[i];
@ -102,11 +102,11 @@ function start() {
} }
} }
if (!settings.autoInstallModules) { if (!settings.autoInstallModules) {
log.info(i18n._("nodes.removing-modules")); log.info(log._("server.removing-modules"));
redNodes.cleanModuleList(); redNodes.cleanModuleList();
} }
} }
log.info(i18n._("runtime.paths.settings",{path:settings.settingsFile})); log.info(log._("runtime.paths.settings",{path:settings.settingsFile}));
redNodes.loadFlows(); redNodes.loadFlows();
comms.start(); comms.start();
}).otherwise(function(err) { }).otherwise(function(err) {
@ -119,7 +119,7 @@ function start() {
function reportAddedModules(info) { function reportAddedModules(info) {
comms.publish("node/added",info.nodes,false); comms.publish("node/added",info.nodes,false);
if (info.nodes.length > 0) { if (info.nodes.length > 0) {
log.info(i18n._("nodes.added-types")); log.info(log._("server.added-types"));
for (var i=0;i<info.nodes.length;i++) { for (var i=0;i<info.nodes.length;i++) {
for (var j=0;j<info.nodes[i].types.length;j++) { for (var j=0;j<info.nodes[i].types.length;j++) {
log.info(" - "+ log.info(" - "+
@ -135,7 +135,7 @@ function reportAddedModules(info) {
function reportRemovedModules(removedNodes) { function reportRemovedModules(removedNodes) {
comms.publish("node/removed",removedNodes,false); comms.publish("node/removed",removedNodes,false);
log.info(i18n._("nodes.removed-types")); log.info(log._("server.removed-types"));
for (var j=0;j<removedNodes.length;j++) { for (var j=0;j<removedNodes.length;j++) {
for (var i=0;i<removedNodes[j].types.length;i++) { for (var i=0;i<removedNodes[j].types.length;i++) {
log.info(" - "+(removedNodes[j].module?removedNodes[j].module+":":"")+removedNodes[j].types[i]); log.info(" - "+(removedNodes[j].module?removedNodes[j].module+":":"")+removedNodes[j].types[i]);
@ -161,7 +161,7 @@ function installModule(module) {
//TODO: ensure module is 'safe' //TODO: ensure module is 'safe'
return when.promise(function(resolve,reject) { return when.promise(function(resolve,reject) {
if (/[\s;]/.test(module)) { if (/[\s;]/.test(module)) {
reject(new Error(i18n._("nodes.install.invalid"))); reject(new Error(log._("server.install.invalid")));
return; return;
} }
if (redNodes.getModuleInfo(module)) { if (redNodes.getModuleInfo(module)) {
@ -182,19 +182,19 @@ function installModule(module) {
if (err) { if (err) {
var lookFor404 = new RegExp(" 404 .*"+module+"$","m"); var lookFor404 = new RegExp(" 404 .*"+module+"$","m");
if (lookFor404.test(stdout)) { if (lookFor404.test(stdout)) {
log.warn(i18n._("nodes.install.install-failed-not-found",{name:module})); log.warn(log._("server.install.install-failed-not-found",{name:module}));
var e = new Error(); var e = new Error();
e.code = 404; e.code = 404;
reject(e); reject(e);
} else { } else {
log.warn(i18n._("nodes.install.install-failed-long",{name:module})); log.warn(log._("server.install.install-failed-long",{name:module}));
log.warn("------------------------------------------"); log.warn("------------------------------------------");
log.warn(err.toString()); log.warn(err.toString());
log.warn("------------------------------------------"); log.warn("------------------------------------------");
reject(new Error(i18n._("nodes.install.install-failed"))); reject(new Error(log._("server.install.install-failed")));
} }
} else { } else {
log.info(i18n._("nodes.install.installed",{name:module})); log.info(log._("server.install.installed",{name:module}));
resolve(redNodes.addModule(module).then(reportAddedModules)); resolve(redNodes.addModule(module).then(reportAddedModules));
} }
} }
@ -205,30 +205,30 @@ function installModule(module) {
function uninstallModule(module) { function uninstallModule(module) {
return when.promise(function(resolve,reject) { return when.promise(function(resolve,reject) {
if (/[\s;]/.test(module)) { if (/[\s;]/.test(module)) {
reject(new Error(i18n._("nodes.install.invalid"))); reject(new Error(log._("server.install.invalid")));
return; return;
} }
var installDir = settings.userDir || process.env.NODE_RED_HOME || "."; var installDir = settings.userDir || process.env.NODE_RED_HOME || ".";
var moduleDir = path.join(installDir,"node_modules",module); var moduleDir = path.join(installDir,"node_modules",module);
if (!fs.existsSync(moduleDir)) { if (!fs.existsSync(moduleDir)) {
return reject(new Error(i18n._("nodes.install.uninstall-failed",{name:module}))); return reject(new Error(log._("server.install.uninstall-failed",{name:module})));
} }
var list = redNodes.removeModule(module); var list = redNodes.removeModule(module);
log.info(i18n._("nodes.install.uninstalling",{name:module})); log.info(log._("server.install.uninstalling",{name:module}));
var child = child_process.exec('npm remove '+module, var child = child_process.exec('npm remove '+module,
{ {
cwd: installDir cwd: installDir
}, },
function(err, stdin, stdout) { function(err, stdin, stdout) {
if (err) { if (err) {
log.warn(i18n._("nodes.install.uninstall-failed-long",{name:module})); log.warn(log._("server.install.uninstall-failed-long",{name:module}));
log.warn("------------------------------------------"); log.warn("------------------------------------------");
log.warn(err.toString()); log.warn(err.toString());
log.warn("------------------------------------------"); log.warn("------------------------------------------");
reject(new Error(i18n._("nodes.install.uninstall-failed",{name:module}))); reject(new Error(log._("server.install.uninstall-failed",{name:module})));
} else { } else {
log.info(i18n._("nodes.install.uninstalled",{name:module})); log.info(log._("server.install.uninstalled",{name:module}));
reportRemovedModules(list); reportRemovedModules(list);
resolve(list); resolve(list);
} }

View File

@ -185,15 +185,15 @@ var localfilesystem = {
getFlows: function() { getFlows: function() {
return when.promise(function(resolve) { return when.promise(function(resolve) {
log.info("User Directory : "+settings.userDir); log.info(log._("storage.localfilesystem.user-dir",{path:settings.userDir}));
log.info("Flows file : "+flowsFullPath); log.info(log._("storage.localfilesystem.flows-file",{path:flowsFullPath}));
fs.exists(flowsFullPath, function(exists) { fs.exists(flowsFullPath, function(exists) {
if (exists) { if (exists) {
resolve(nodeFn.call(fs.readFile,flowsFullPath,'utf8').then(function(data) { resolve(nodeFn.call(fs.readFile,flowsFullPath,'utf8').then(function(data) {
return JSON.parse(data); return JSON.parse(data);
})); }));
} else { } else {
log.info("Creating new flows file"); log.info(log._("storage.localfilesystem.create"));
resolve([]); resolve([]);
} }
}); });
@ -257,7 +257,7 @@ var localfilesystem = {
try { try {
return JSON.parse(data); return JSON.parse(data);
} catch(err) { } catch(err) {
log.info("Corrupted config detected - resetting"); log.trace("Corrupted config detected - resetting");
return {}; return {};
} }
} else { } else {
@ -277,7 +277,7 @@ var localfilesystem = {
try { try {
return JSON.parse(data); return JSON.parse(data);
} catch(err) { } catch(err) {
log.info("Corrupted sessions file - resetting"); log.trace("Corrupted sessions file - resetting");
return {}; return {};
} }
} else { } else {

View File

@ -29,7 +29,7 @@ var auth = require("../../../red/api/auth");
describe('Credentials', function() { describe('Credentials', function() {
afterEach(function() { afterEach(function() {
index.clearRegistry(); index.clearRegistry();
}); });
@ -156,7 +156,7 @@ describe('Credentials', function() {
credentials.init(storage); credentials.init(storage);
credentials.load().then(function() { credentials.load().then(function() {
logmsg.should.equal("Error loading credentials : test forcing failure"); log.warn.calledOnce.should.be.true;
log.warn.restore(); log.warn.restore();
done(); done();
}).otherwise(function(err){ }).otherwise(function(err){
@ -218,7 +218,7 @@ describe('Credentials', function() {
index.loadFlows().then(function() { index.loadFlows().then(function() {
var testnode = new TestNode({id:'tab1',type:'test',name:'barney'}); var testnode = new TestNode({id:'tab1',type:'test',name:'barney'});
credentials.extract(testnode); credentials.extract(testnode);
should.equal(logmsg, 'Credential Type test is not registered.'); log.warn.calledOnce.should.be.true;
log.warn.restore(); log.warn.restore();
done(); done();
}).otherwise(function(err){ }).otherwise(function(err){