Merge pull request #2665 from node-red/msg-router

Pluggable Message Routing
This commit is contained in:
Nick O'Leary
2020-09-29 20:36:23 +01:00
committed by GitHub
22 changed files with 1576 additions and 560 deletions

View File

@@ -37,7 +37,7 @@ describe("runtime-api/flows", function() {
it("returns the current flow configuration", function(done) {
flows.init({
log: mockLog(),
nodes: {
flows: {
getFlows: function() { return [1,2,3] }
}
});
@@ -76,7 +76,7 @@ describe("runtime-api/flows", function() {
})
flows.init({
log: mockLog(),
nodes: {
flows: {
getFlows: function() { return {rev:"currentRev",flows:[]} },
setFlows: setFlows,
loadFlows: loadFlows
@@ -192,7 +192,7 @@ describe("runtime-api/flows", function() {
});
flows.init({
log: mockLog(),
nodes: {
flows: {
addFlow: addFlow
}
});
@@ -225,7 +225,7 @@ describe("runtime-api/flows", function() {
});
flows.init({
log: mockLog(),
nodes: {
flows: {
getFlow: getFlow
}
});
@@ -255,7 +255,9 @@ describe("runtime-api/flows", function() {
var err = new Error();
// TODO: quirk of internal api - uses .code for .status
err.code = 404;
throw err;
var p = Promise.reject(err);
p.catch(()=>{});
return p;
} else if (id === "error") {
var err = new Error();
// TODO: quirk of internal api - uses .code for .status
@@ -268,7 +270,7 @@ describe("runtime-api/flows", function() {
});
flows.init({
log: mockLog(),
nodes: {
flows: {
updateFlow: updateFlow
}
});
@@ -311,7 +313,9 @@ describe("runtime-api/flows", function() {
var err = new Error();
// TODO: quirk of internal api - uses .code for .status
err.code = 404;
throw err;
var p = Promise.reject(err);
p.catch(()=>{});
return p;
} else if (flow === "error") {
var err = new Error();
// TODO: quirk of internal api - uses .code for .status
@@ -324,7 +328,7 @@ describe("runtime-api/flows", function() {
});
flows.init({
log: mockLog(),
nodes: {
flows: {
removeFlow: removeFlow
}
});

View File

@@ -22,10 +22,11 @@ var util = require("util");
var NR_TEST_UTILS = require("nr-test-utils");
var flowUtils = NR_TEST_UTILS.require("@node-red/runtime/lib/nodes/flows/util");
var Flow = NR_TEST_UTILS.require("@node-red/runtime/lib/nodes/flows/Flow");
var flows = NR_TEST_UTILS.require("@node-red/runtime/lib/nodes/flows");
var flowUtils = NR_TEST_UTILS.require("@node-red/runtime/lib/flows/util");
var Flow = NR_TEST_UTILS.require("@node-red/runtime/lib/flows/Flow");
var flows = NR_TEST_UTILS.require("@node-red/runtime/lib/flows");
var Node = NR_TEST_UTILS.require("@node-red/runtime/lib/nodes/Node");
var hooks = NR_TEST_UTILS.require("@node-red/runtime/lib/hooks");
var typeRegistry = NR_TEST_UTILS.require("@node-red/registry");
@@ -826,4 +827,371 @@ describe('Flow', function() {
});
describe("#send", function() {
it("sends a message - no cloning", function(done) {
var shutdownTest = function(err) {
hooks.clear();
flow.stop().then(() => { done(err) });
}
var config = flowUtils.parseConfig([
{id:"t1",type:"tab"},
{id:"1",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["2"]},
{id:"2",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["3"]}
]);
var flow = Flow.create({},config,config.flows["t1"]);
flow.start();
Object.keys(flow.getActiveNodes()).should.have.length(2);
var n1 = flow.getNode('1');
var n2 = flow.getNode('2');
var messageReceived = false;
n2.receive = function(msg) {
messageReceived = true;
try {
msg.should.be.exactly(message);
shutdownTest();
} catch(err) {
shutdownTest(err);
}
}
var message = {payload:"hello"}
flow.send([{
msg: message,
source: { id:"1", node: n1 },
destination: { id:"2", node: undefined },
cloneMessage: false
}])
messageReceived.should.be.false()
})
it("sends a message - cloning", function(done) {
var shutdownTest = function(err) {
hooks.clear();
flow.stop().then(() => { done(err) });
}
var config = flowUtils.parseConfig([
{id:"t1",type:"tab"},
{id:"1",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["2"]},
{id:"2",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["3"]}
]);
var flow = Flow.create({},config,config.flows["t1"]);
flow.start();
Object.keys(flow.getActiveNodes()).should.have.length(2);
var n1 = flow.getNode('1');
var n2 = flow.getNode('2');
n2.receive = function(msg) {
try {
// Message should be cloned
msg.should.be.eql(message);
msg.should.not.be.exactly(message);
shutdownTest();
} catch(err) {
shutdownTest(err);
}
}
var message = {payload:"hello"}
flow.send([{
msg: message,
source: { id:"1", node: n1 },
destination: { id:"2", node: undefined },
cloneMessage: true
}])
})
it("sends multiple messages", function(done) {
var shutdownTest = function(err) {
hooks.clear();
flow.stop().then(() => { done(err) });
}
var config = flowUtils.parseConfig([
{id:"t1",type:"tab"},
{id:"1",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["2"]},
{id:"2",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["3"]}
]);
var flow = Flow.create({},config,config.flows["t1"]);
flow.start();
Object.keys(flow.getActiveNodes()).should.have.length(2);
var n1 = flow.getNode('1');
var n2 = flow.getNode('2');
var messageCount = 0;
n2.receive = function(msg) {
try {
msg.should.be.exactly(messages[messageCount++]);
if (messageCount === 2) {
shutdownTest();
}
} catch(err) {
shutdownTest(err);
}
}
var messages = [{payload:"hello"},{payload:"world"}];
flow.send([{
msg: messages[0],
source: { id:"1", node: n1 },
destination: { id:"2", node: undefined }
},{
msg: messages[1],
source: { id:"1", node: n1 },
destination: { id:"2", node: undefined }
}])
})
it("sends a message - triggers hooks", function(done) {
var hookErrors = [];
var messageReceived = false;
var hooksCalled = [];
hooks.add("onSend", function(sendEvents) {
hooksCalled.push("onSend")
try {
messageReceived.should.be.false()
sendEvents.should.have.length(1);
sendEvents[0].msg.should.be.exactly(message);
} catch(err) {
hookErrors.push(err);
}
})
hooks.add("preRoute", function(sendEvent) {
hooksCalled.push("preRoute")
try {
messageReceived.should.be.false()
sendEvent.msg.should.be.exactly(message);
should.not.exist(sendEvent.destination.node)
} catch(err) {
hookErrors.push(err);
}
})
hooks.add("preDeliver", function(sendEvent) {
hooksCalled.push("preDeliver")
try {
messageReceived.should.be.false()
// Cloning should have happened
sendEvent.msg.should.not.be.exactly(message);
// Destinatino node populated
should.exist(sendEvent.destination.node)
} catch(err) {
hookErrors.push(err);
}
})
hooks.add("postDeliver", function(sendEvent) {
hooksCalled.push("postDeliver")
try {
messageReceived.should.be.false()
} catch(err) {
hookErrors.push(err);
}
})
var shutdownTest = function(err) {
hooks.clear();
flow.stop().then(() => { done(err) });
}
var config = flowUtils.parseConfig([
{id:"t1",type:"tab"},
{id:"1",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["2"]},
{id:"2",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["3"]}
]);
var flow = Flow.create({},config,config.flows["t1"]);
flow.start();
Object.keys(flow.getActiveNodes()).should.have.length(2);
var n1 = flow.getNode('1');
var n2 = flow.getNode('2');
n2.receive = function(msg) {
messageReceived = true;
try {
msg.should.be.eql(message);
msg.should.not.be.exactly(message);
hooksCalled.should.eql(["onSend","preRoute","preDeliver","postDeliver"])
if (hookErrors.length > 0) {
shutdownTest(hookErrors[0])
} else {
shutdownTest();
}
} catch(err) {
shutdownTest(err);
}
}
var message = {payload:"hello"}
flow.send([{
msg: message,
source: { id:"1", node: n1 },
destination: { id:"2", node: undefined },
cloneMessage: true
}])
})
describe("errors thrown by hooks are reported to the sending node", function() {
var flow;
var n1,n2;
var messageReceived = false;
var errorReceived = null;
before(function() {
hooks.add("onSend", function(sendEvents) {
if (sendEvents[0].msg.payload === "trigger-onSend") {
throw new Error("onSend Error");
}
})
hooks.add("preRoute", function(sendEvent) {
if (sendEvent.msg.payload === "trigger-preRoute") {
throw new Error("preRoute Error");
}
})
hooks.add("preDeliver", function(sendEvent) {
if (sendEvent.msg.payload === "trigger-preDeliver") {
throw new Error("preDeliver Error");
}
})
hooks.add("postDeliver", function(sendEvent) {
if (sendEvent.msg.payload === "trigger-postDeliver") {
throw new Error("postDeliver Error");
}
})
var config = flowUtils.parseConfig([
{id:"t1",type:"tab"},
{id:"1",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["2"]},
{id:"2",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["3"]}
]);
flow = Flow.create({},config,config.flows["t1"]);
flow.start();
n1 = flow.getNode('1');
n2 = flow.getNode('2');
n2.receive = function(msg) {
messageReceived = true;
}
n1.error = function(err) {
errorReceived = err;
}
})
after(function(done) {
hooks.clear();
flow.stop().then(() => { done() });
})
beforeEach(function() {
messageReceived = false;
errorReceived = null;
})
function testHook(hook, msgExpected, done) {
var message = {payload:"trigger-"+hook}
flow.send([{
msg: message,
source: { id:"1", node: n1 },
destination: { id:"2", node: undefined },
cloneMessage: true
}])
setTimeout(function() {
try {
messageReceived.should.equal(msgExpected);
should.exist(errorReceived)
errorReceived.toString().should.containEql(hook);
done();
} catch(err) {
done(err);
}
},10)
}
it("onSend", function(done) { testHook("onSend", false, done) })
it("preRoute", function(done) { testHook("preRoute", false, done) })
it("preDeliver", function(done) { testHook("preDeliver", false, done) })
it("postDeliver", function(done) { testHook("postDeliver", true, done) })
})
describe("hooks can stop the sending of messages", function() {
var flow;
var n1,n2;
var messageReceived = false;
var errorReceived = false;
before(function() {
hooks.add("onSend", function(sendEvents) {
if (sendEvents[0].msg.payload === "trigger-onSend") {
return false
}
})
hooks.add("preRoute", function(sendEvent) {
if (sendEvent.msg.payload === "trigger-preRoute") {
return false
}
})
hooks.add("preDeliver", function(sendEvent) {
if (sendEvent.msg.payload === "trigger-preDeliver") {
return false
}
})
hooks.add("postDeliver", function(sendEvent) {
if (sendEvent.msg.payload === "trigger-postDeliver") {
return false
}
})
var config = flowUtils.parseConfig([
{id:"t1",type:"tab"},
{id:"1",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["2"]},
{id:"2",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["3"]}
]);
flow = Flow.create({},config,config.flows["t1"]);
flow.start();
n1 = flow.getNode('1');
n2 = flow.getNode('2');
n2.receive = function(msg) {
messageReceived = true;
}
n1.error = function(err) {
errorReceived = true;
}
})
after(function(done) {
hooks.clear();
flow.stop().then(() => { done() });
})
function testSend(payload,messageReceivedExpected,errorReceivedExpected,done) {
messageReceived = false;
errorReceived = false;
flow.send([{
msg: {payload: payload},
source: { id:"1", node: n1 },
destination: { id:"2", node: undefined },
cloneMessage: true
}])
setTimeout(function() {
try {
messageReceived.should.eql(messageReceivedExpected)
errorReceived.should.eql(errorReceivedExpected)
done();
} catch(err) {
done(err);
}
},10)
}
function testHook(hook, done) {
testSend("pass",true,false,err => {
if (err) {
done(err)
} else {
testSend("trigger-"+hook,false,false,done);
}
})
}
it("onSend", function(done) { testHook("onSend", done) })
it("preRoute", function(done) { testHook("preRoute", done) })
it("preDeliver", function(done) { testHook("preDeliver", done) })
// postDeliver happens after delivery is scheduled so cannot stop it
// it("postDeliver", function(done) { testHook("postDeliver", done) })
})
})
});

View File

@@ -22,11 +22,11 @@ var util = require("util");
var NR_TEST_UTILS = require("nr-test-utils");
var Subflow = NR_TEST_UTILS.require("@node-red/runtime/lib/nodes/flows/Subflow");
var Flow = NR_TEST_UTILS.require("@node-red/runtime/lib/nodes/flows/Flow");
var Subflow = NR_TEST_UTILS.require("@node-red/runtime/lib/flows/Subflow");
var Flow = NR_TEST_UTILS.require("@node-red/runtime/lib/flows/Flow");
var flowUtils = NR_TEST_UTILS.require("@node-red/runtime/lib/nodes/flows/util");
var flows = NR_TEST_UTILS.require("@node-red/runtime/lib/nodes/flows");
var flowUtils = NR_TEST_UTILS.require("@node-red/runtime/lib/flows/util");
var flows = NR_TEST_UTILS.require("@node-red/runtime/lib/flows");
var Node = NR_TEST_UTILS.require("@node-red/runtime/lib/nodes/Node");
var typeRegistry = NR_TEST_UTILS.require("@node-red/registry");

View File

@@ -20,13 +20,13 @@ var when = require("when");
var clone = require("clone");
var NR_TEST_UTILS = require("nr-test-utils");
var flows = NR_TEST_UTILS.require("@node-red/runtime/lib/nodes/flows");
var flows = NR_TEST_UTILS.require("@node-red/runtime/lib/flows");
var RedNode = NR_TEST_UTILS.require("@node-red/runtime/lib/nodes/Node");
var RED = NR_TEST_UTILS.require("@node-red/runtime/lib/nodes");
var events = NR_TEST_UTILS.require("@node-red/runtime/lib/events");
var credentials = NR_TEST_UTILS.require("@node-red/runtime/lib/nodes/credentials");
var typeRegistry = NR_TEST_UTILS.require("@node-red/registry")
var Flow = NR_TEST_UTILS.require("@node-red/runtime/lib/nodes/flows/Flow");
var Flow = NR_TEST_UTILS.require("@node-red/runtime/lib/flows/Flow");
describe('flows/index', function() {
@@ -209,8 +209,7 @@ describe('flows/index', function() {
storage.getFlows = function() {
return when.resolve({flows:originalConfig});
}
events.once('nodes-started',function() {
events.once('flows:started',function() {
flows.setFlows(newConfig,"nodes").then(function() {
flows.getFlows().flows.should.eql(newConfig);
flowCreate.flows['t1'].update.called.should.be.true();
@@ -239,7 +238,7 @@ describe('flows/index', function() {
return when.resolve({flows:originalConfig});
}
events.once('nodes-started',function() {
events.once('flows:started',function() {
flows.setFlows(newConfig,"nodes").then(function() {
flows.getFlows().flows.should.eql(newConfig);
flowCreate.flows['t1'].update.called.should.be.true();
@@ -301,7 +300,7 @@ describe('flows/index', function() {
return when.resolve({flows:originalConfig});
}
events.once('nodes-started',function() {
events.once('flows:started',function() {
Object.keys(flowCreate.flows).should.eql(['_GLOBAL_','t1']);
done();
});
@@ -398,7 +397,7 @@ describe('flows/index', function() {
// return when.resolve({flows:originalConfig});
// }
//
// events.once('nodes-started',function() {
// events.once('flows:started',function() {
// flows.handleError(originalConfig[0],"message",{});
// flowCreate.flows['t1'].handleError.called.should.be.true();
// done();
@@ -423,7 +422,7 @@ describe('flows/index', function() {
// return when.resolve({flows:originalConfig});
// }
//
// events.once('nodes-started',function() {
// events.once('flows:started',function() {
// flows.handleError(originalConfig[0],"message",{});
// try {
// flowCreate.flows['t1'].handleError.called.should.be.true();
@@ -451,7 +450,7 @@ describe('flows/index', function() {
// return when.resolve({flows:originalConfig});
// }
//
// events.once('nodes-started',function() {
// events.once('flows:started',function() {
// flows.handleStatus(originalConfig[0],"message");
// flowCreate.flows['t1'].handleStatus.called.should.be.true();
// done();
@@ -477,7 +476,7 @@ describe('flows/index', function() {
// return when.resolve({flows:originalConfig});
// }
//
// events.once('nodes-started',function() {
// events.once('flows:started',function() {
// flows.handleStatus(originalConfig[0],"message");
// try {
// flowCreate.flows['t1'].handleStatus.called.should.be.true();

View File

@@ -19,7 +19,7 @@ var sinon = require("sinon");
var when = require("when");
var clone = require("clone");
var NR_TEST_UTILS = require("nr-test-utils");
var flowUtil = NR_TEST_UTILS.require("@node-red/runtime/lib/nodes/flows/util");
var flowUtil = NR_TEST_UTILS.require("@node-red/runtime/lib/flows/util");
var typeRegistry = NR_TEST_UTILS.require("@node-red/registry");
var redUtil = NR_TEST_UTILS.require("@node-red/util").util;

View File

@@ -0,0 +1,252 @@
const should = require("should");
const NR_TEST_UTILS = require("nr-test-utils");
const hooks = NR_TEST_UTILS.require("@node-red/runtime/lib/hooks");
describe("runtime/hooks", function() {
afterEach(function() {
hooks.clear();
})
it("allows a hook to be registered", function(done) {
let calledWith = null;
hooks.has("onSend").should.be.false();
hooks.add("onSend", function(payload) { calledWith = payload } )
hooks.has("onSend").should.be.true();
let data = { a: 1 };
hooks.trigger("onSend",data,err => {
calledWith.should.equal(data);
done(err);
})
})
it("rejects invalid hook id", function(done) {
try {
hooks.add("foo", function(payload) {})
done(new Error("Invalid hook accepted"))
} catch(err) {
done();
}
})
it("calls hooks in the order they were registered", function(done) {
hooks.add("onSend", function(payload) { payload.order.push("A") } )
hooks.add("onSend", function(payload) { payload.order.push("B") } )
let data = { order:[] };
hooks.trigger("onSend",data,err => {
data.order.should.eql(["A","B"])
done(err);
})
})
it("does not allow multiple hooks with same id.label", function() {
hooks.has("onSend.one").should.be.false();
hooks.has("onSend").should.be.false();
hooks.add("onSend.one", function(payload) { payload.order.push("A") } );
hooks.has("onSend.one").should.be.true();
hooks.has("onSend").should.be.true();
(function() {
hooks.add("onSend.one", function(payload) { payload.order.push("B") } )
}).should.throw();
})
it("removes labelled hook", function(done) {
hooks.has("onSend.A").should.be.false();
hooks.has("onSend.B").should.be.false();
hooks.has("onSend").should.be.false();
hooks.add("onSend.A", function(payload) { payload.order.push("A") } )
hooks.has("onSend.A").should.be.true();
hooks.has("onSend.B").should.be.false();
hooks.has("onSend").should.be.true();
hooks.add("onSend.B", function(payload) { payload.order.push("B") } )
hooks.has("onSend.A").should.be.true();
hooks.has("onSend.B").should.be.true();
hooks.has("onSend").should.be.true();
hooks.remove("onSend.A");
hooks.has("onSend.A").should.be.false();
hooks.has("onSend.B").should.be.true();
hooks.has("onSend").should.be.true();
let data = { order:[] };
hooks.trigger("onSend",data,err => {
try {
data.order.should.eql(["B"])
hooks.remove("onSend.B");
hooks.has("onSend.A").should.be.false();
hooks.has("onSend.B").should.be.false();
hooks.has("onSend").should.be.false();
done(err);
} catch(err2) {
done(err2);
}
})
})
it("cannot remove unlabelled hook", function() {
hooks.add("onSend", function(payload) { payload.order.push("A") } );
(function() {
hooks.remove("onSend")
}).should.throw();
})
it("removes all hooks with same label", function(done) {
hooks.add("onSend.A", function(payload) { payload.order.push("A") } )
hooks.add("onSend.B", function(payload) { payload.order.push("B") } )
hooks.add("preRoute.A", function(payload) { payload.order.push("C") } )
hooks.add("preRoute.B", function(payload) { payload.order.push("D") } )
let data = { order:[] };
hooks.trigger("onSend",data,err => {
data.order.should.eql(["A","B"])
hooks.trigger("preRoute", data, err => {
data.order.should.eql(["A","B","C","D"])
data.order = [];
hooks.remove("*.A");
hooks.trigger("onSend",data,err => {
data.order.should.eql(["B"])
hooks.trigger("preRoute", data, err => {
data.order.should.eql(["B","D"])
})
done(err);
})
})
})
})
it("halts execution on return false", function(done) {
hooks.add("onSend.A", function(payload) { payload.order.push("A"); return false } )
hooks.add("onSend.B", function(payload) { payload.order.push("B") } )
let data = { order:[] };
hooks.trigger("onSend",data,err => {
data.order.should.eql(["A"])
err.should.be.false();
done();
})
})
it("halts execution on thrown error", function(done) {
hooks.add("onSend.A", function(payload) { payload.order.push("A"); throw new Error("error") } )
hooks.add("onSend.B", function(payload) { payload.order.push("B") } )
let data = { order:[] };
hooks.trigger("onSend",data,err => {
data.order.should.eql(["A"])
should.exist(err);
err.should.not.be.false()
done();
})
})
it("handler can use callback function", function(done) {
hooks.add("onSend.A", function(payload, done) {
setTimeout(function() {
payload.order.push("A")
done()
},30)
})
hooks.add("onSend.B", function(payload) { payload.order.push("B") } )
let data = { order:[] };
hooks.trigger("onSend",data,err => {
data.order.should.eql(["A","B"])
done(err);
})
})
it("handler can use callback function - halt execution", function(done) {
hooks.add("onSend.A", function(payload, done) {
setTimeout(function() {
payload.order.push("A")
done(false)
},30)
})
hooks.add("onSend.B", function(payload) { payload.order.push("B") } )
let data = { order:[] };
hooks.trigger("onSend",data,err => {
data.order.should.eql(["A"])
err.should.be.false()
done();
})
})
it("handler can use callback function - halt on error", function(done) {
hooks.add("onSend.A", function(payload, done) {
setTimeout(function() {
done(new Error("test error"))
},30)
})
hooks.add("onSend.B", function(payload) { payload.order.push("B") } )
let data = { order:[] };
hooks.trigger("onSend",data,err => {
data.order.should.eql([])
should.exist(err);
err.should.not.be.false()
done();
})
})
it("handler be an async function", function(done) {
hooks.add("onSend.A", async function(payload) {
return new Promise(resolve => {
setTimeout(function() {
payload.order.push("A")
resolve()
},30)
});
})
hooks.add("onSend.B", function(payload) { payload.order.push("B") } )
let data = { order:[] };
hooks.trigger("onSend",data,err => {
data.order.should.eql(["A","B"])
done(err);
})
})
it("handler be an async function - halt execution", function(done) {
hooks.add("onSend.A", async function(payload) {
return new Promise(resolve => {
setTimeout(function() {
payload.order.push("A")
resolve(false)
},30)
});
})
hooks.add("onSend.B", function(payload) { payload.order.push("B") } )
let data = { order:[] };
hooks.trigger("onSend",data,err => {
data.order.should.eql(["A"])
done(err);
})
})
it("handler be an async function - halt on error", function(done) {
hooks.add("onSend.A", async function(payload) {
return new Promise((resolve,reject) => {
setTimeout(function() {
reject(new Error("test error"))
},30)
});
})
hooks.add("onSend.B", function(payload) { payload.order.push("B") } )
let data = { order:[] };
hooks.trigger("onSend",data,err => {
data.order.should.eql([])
should.exist(err);
err.should.not.be.false()
done();
})
})
});

View File

@@ -19,7 +19,8 @@ var sinon = require('sinon');
var NR_TEST_UTILS = require("nr-test-utils");
var RedNode = NR_TEST_UTILS.require("@node-red/runtime/lib/nodes/Node");
var Log = NR_TEST_UTILS.require("@node-red/util").log;
var flows = NR_TEST_UTILS.require("@node-red/runtime/lib/nodes/flows");
var hooks = NR_TEST_UTILS.require("@node-red/runtime/lib/hooks");
var flows = NR_TEST_UTILS.require("@node-red/runtime/lib/flows");
describe('Node', function() {
describe('#constructor',function() {
@@ -181,6 +182,93 @@ describe('Node', function() {
});
n.receive(message);
});
it('triggers onComplete hook when done callback provided', function(done) {
var handleCompleteCalled = false;
var hookCalled = false;
var n = new RedNode({id:'123',type:'abc', _flow: {
handleComplete: function(node,msg) {
handleCompleteCalled = true;
}
}});
var hookError;
hooks.add("onComplete",function(completeEvent) {
hookCalled = true;
try {
handleCompleteCalled.should.be.false("onComplete should be called before handleComplete")
should.not.exist(completeEvent.error);
completeEvent.msg.should.deepEqual(message);
completeEvent.node.id.should.eql("123");
completeEvent.node.node.should.equal(n);
} catch(err) {
hookError = err;
}
})
var message = {payload:"hello world"};
n.on('input',function(msg, nodeSend, nodeDone) {
nodeDone();
});
n.receive(message);
setTimeout(function() {
if (hookError) {
done(hookError);
return
}
try {
hookCalled.should.be.true("onComplete hook should be called");
handleCompleteCalled.should.be.true("handleComplete should be called");
done();
} catch(err) {
done(err);
}
})
});
it('triggers onComplete hook when done callback provided - with error', function(done) {
var handleCompleteCalled = false;
var hookCalled = false;
var errorReported = false;
var n = new RedNode({id:'123',type:'abc', _flow: {
handleComplete: function(node,msg) {
handleCompleteCalled = true;
}
}});
var hookError;
hooks.add("onComplete",function(completeEvent) {
hookCalled = true;
try {
handleCompleteCalled.should.be.false("onComplete should be called before handleComplete")
should.exist(completeEvent.error);
completeEvent.error.toString().should.equal("Error: test error")
completeEvent.msg.should.deepEqual(message);
completeEvent.node.id.should.eql("123");
completeEvent.node.node.should.equal(n);
} catch(err) {
hookError = err;
}
})
var message = {payload:"hello world"};
n.on('input',function(msg, nodeSend, nodeDone) {
nodeDone(new Error("test error"));
});
n.error = function(err,msg) {
errorReported = true;
}
n.receive(message);
setTimeout(function() {
if (hookError) {
done(hookError);
return
}
try {
hookCalled.should.be.true("onComplete hook should be called");
handleCompleteCalled.should.be.false("handleComplete should not be called");
done();
} catch(err) {
done(err);
}
})
});
it('logs error if callback provides error', function(done) {
var n = new RedNode({id:'123',type:'abc'});
sinon.stub(n,"error",function(err,msg) {});
@@ -201,151 +289,256 @@ describe('Node', function() {
});
n.receive(message);
});
it("triggers hooks when receiving a message", function(done) {
var hookErrors = [];
var messageReceived = false;
var hooksCalled = [];
hooks.add("onReceive", function(receiveEvent) {
hooksCalled.push("onReceive")
try {
messageReceived.should.be.false("Message should not have been received before onReceive")
receiveEvent.msg.should.be.exactly(message);
receiveEvent.destination.id.should.equal("123")
receiveEvent.destination.node.should.equal(n)
} catch(err) {
hookErrors.push(err);
}
})
hooks.add("postReceive", function(receiveEvent) {
hooksCalled.push("postReceive")
try {
messageReceived.should.be.true("Message should have been received before postReceive")
receiveEvent.msg.should.be.exactly(message);
receiveEvent.destination.id.should.equal("123")
receiveEvent.destination.node.should.equal(n)
} catch(err) {
hookErrors.push(err);
}
})
var n = new RedNode({id:'123',type:'abc'});
var message = {payload:"hello world"};
n.on('input',function(msg) {
messageReceived = true;
try {
should.strictEqual(this,n);
hooksCalled.should.eql(["onReceive"])
should.deepEqual(msg,message);
} catch(err) {
hookErrors.push(err)
}
});
n.receive(message);
setTimeout(function() {
hooks.clear();
if (hookErrors.length > 0) {
done(hookErrors[0])
} else {
done();
}
},10);
});
describe("errors thrown by hooks are reported", function() {
before(function() {
hooks.add("onReceive",function(recEvent) {
if (recEvent.msg.payload === "trigger-onReceive") {
throw new Error("onReceive Error")
}
})
hooks.add("postReceive",function(recEvent) {
if (recEvent.msg.payload === "trigger-postReceive") {
throw new Error("postReceive Error")
}
})
})
after(function() {
hooks.clear();
})
function testHook(hook, msgExpected, done) {
var messageReceived = false;
var errorReceived;
var n = new RedNode({id:'123',type:'abc'});
var message = {payload:"trigger-"+hook};
n.on('input',function(msg) {
messageReceived = true;
});
n.error = function (err) {
errorReceived = err;
}
n.receive(message);
setTimeout(function() {
try {
messageReceived.should.equal(msgExpected,`Hook ${hook} messageReceived expected ${msgExpected} actual ${messageReceived}`);
should.exist(errorReceived);
errorReceived.toString().should.containEql(hook)
done()
} catch(err) {
done(err);
}
},10);
}
it("onReceive", function(done) { testHook("onReceive", false, done)})
it("postReceive", function(done) { testHook("postReceive", true, done)})
})
});
describe("hooks can halt receive", function() {
before(function() {
hooks.add("onReceive",function(recEvent) {
if (recEvent.msg.payload === "trigger-onReceive") {
return false;
}
})
})
after(function() {
hooks.clear();
})
function testHook(hook, msgExpected, done) {
var messageReceived = false;
var errorReceived;
var n = new RedNode({id:'123',type:'abc'});
var message = {payload:"trigger-"+hook};
n.on('input',function(msg) {
messageReceived = true;
});
n.error = function (err) {
errorReceived = err;
}
n.receive(message);
setTimeout(function() {
try {
messageReceived.should.equal(msgExpected,`Hook ${hook} messageReceived expected ${msgExpected} actual ${messageReceived}`);
should.not.exist(errorReceived);
done()
} catch(err) {
done(err);
}
},10);
}
it("onReceive", function(done) { testHook("onReceive", false, done)})
})
describe('#send', function() {
it('emits a single message', function(done) {
var flow = {
getNode: (id) => { return {'n1':n1,'n2':n2}[id]},
send: (sendEvents) => {
try {
sendEvents.should.have.length(1);
sendEvents[0].msg.should.equal(message);
sendEvents[0].destination.should.eql({id:"n2", node: undefined});
sendEvents[0].source.should.eql({id:"n1", node: n1, port: 0})
done();
} catch(err) {
done(err);
}
},
};
var n1 = new RedNode({_flow:flow,id:'n1',type:'abc',wires:[['n2']]});
var n2 = new RedNode({_flow:flow,id:'n2',type:'abc'});
var message = {payload:"hello world"};
var messageReceived = false;
n2.on('input',function(msg) {
// msg equals message, and is not a new copy
messageReceived = true;
should.strictEqual(this,n2);
should.deepEqual(msg,message);
should.strictEqual(msg,message);
done();
});
n1.send(message);
messageReceived.should.be.false();
});
it('emits a single message - multiple input event listeners', function(done) {
var flow = {
getNode: (id) => { return {'n1':n1,'n2':n2}[id]},
};
var n1 = new RedNode({_flow:flow,id:'n1',type:'abc',wires:[['n2']]});
var n2 = new RedNode({_flow:flow,id:'n2',type:'abc'});
var message = {payload:"hello world"};
var messageReceived = 0;
n2.on('input',function(msg) {
// msg equals message, and is not a new copy
messageReceived++;
messageReceived.should.be.exactly(1);
should.strictEqual(this,n2);
should.deepEqual(msg,message);
should.strictEqual(msg,message);
});
n2.on('input',function(msg) {
messageReceived++;
messageReceived.should.be.exactly(2);
should.strictEqual(this,n2);
should.deepEqual(msg,message);
should.strictEqual(msg,message);
done();
});
n1.send(message);
messageReceived.should.be.exactly(0);
});
it('emits a single message - synchronous mode', function(done) {
var flow = {
getNode: (id) => { return {'n1':n1,'n2':n2}[id]},
asyncMessageDelivery: false
};
var n1 = new RedNode({_flow:flow,id:'n1',type:'abc',wires:[['n2']]});
var n2 = new RedNode({_flow:flow,id:'n2',type:'abc'});
var message = {payload:"hello world"};
var messageReceived = false;
var notSyncErr;
n2.on('input',function(msg) {
try {
// msg equals message, and is not a new copy
messageReceived = true;
should.deepEqual(msg,message);
should.strictEqual(msg,message);
done(notSyncErr);
} catch(err) {
done(err);
}
});
n1.send(message);
try {
messageReceived.should.be.true();
} catch(err) {
notSyncErr = err;
}
});
it('emits a message with callback provided send', function(done) {
var flow = {
getNode: (id) => { return {'n1':n1,'n2':n2}[id]},
handleComplete: (node,msg) => {}
handleError: (node,logMessage,msg,reportingNode) => {done(logMessage)},
handleComplete: (node,msg) => {},
send: (sendEvents) => {
try {
sendEvents.should.have.length(1);
sendEvents[0].msg.should.equal(message);
sendEvents[0].destination.should.eql({id:"n2", node: undefined});
sendEvents[0].source.should.eql({id:"n1", node: n1, port: 0});
sendEvents[0].cloneMessage.should.be.false();
done();
} catch(err) {
done(err);
}
},
};
var n1 = new RedNode({_flow:flow,id:'n1',type:'abc',wires:[['n2']]});
var n2 = new RedNode({_flow:flow,id:'n2',type:'abc'});
var message = {payload:"hello world"};
var messageReceived = false;
n1.on('input',function(msg,nodeSend,nodeDone) {
nodeSend(msg);
nodeDone();
});
n2.on('input',function(msg) {
// msg equals message, and is not a new copy
messageReceived = true;
should.deepEqual(msg,message);
should.strictEqual(msg,message);
done();
});
n1.receive(message);
messageReceived.should.be.false();
});
it('emits multiple messages on a single output', function(done) {
var flow = {
getNode: (id) => { return {'n1':n1,'n2':n2}[id]},
handleError: (node,logMessage,msg,reportingNode) => {done(logMessage)},
send: (sendEvents) => {
try {
sendEvents.should.have.length(2);
sendEvents[0].msg.should.equal(messages[0]);
sendEvents[0].destination.should.eql({id:"n2", node: undefined});
sendEvents[0].source.should.eql({id:"n1", node: n1, port: 0});
sendEvents[0].cloneMessage.should.be.false();
sendEvents[1].msg.should.equal(messages[1]);
sendEvents[1].destination.should.eql({id:"n2", node: undefined});
sendEvents[1].source.should.eql({id:"n1", node: n1, port: 0});
sendEvents[1].cloneMessage.should.be.true();
done();
} catch(err) {
done(err);
}
},
};
var n1 = new RedNode({_flow:flow,id:'n1',type:'abc',wires:[['n2']]});
var n2 = new RedNode({_flow:flow,id:'n2',type:'abc'});
var messages = [
{payload:"hello world"},
{payload:"hello world again"}
];
var rcvdCount = 0;
n2.on('input',function(msg) {
if (rcvdCount === 0) {
// first msg sent, don't clone
should.deepEqual(msg,messages[rcvdCount]);
should.strictEqual(msg,messages[rcvdCount]);
rcvdCount += 1;
} else {
// second msg sent, clone
msg.payload.should.equal(messages[rcvdCount].payload);
should.notStrictEqual(msg,messages[rcvdCount]);
done();
}
});
n1.send([messages]);
});
it('emits messages to multiple outputs', function(done) {
var flow = {
getNode: (id) => { return {'n1':n1,'n2':n2,'n3':n3,'n4':n4,'n5':n5}[id]},
handleError: (node,logMessage,msg,reportingNode) => {done(logMessage)},
send: (sendEvents) => {
try {
sendEvents.should.have.length(3);
sendEvents[0].msg.should.equal(messages[0]);
sendEvents[0].destination.should.eql({id:"n2", node: undefined});
sendEvents[0].source.should.eql({id:"n1", node: n1, port: 0});
sendEvents[0].cloneMessage.should.be.false();
should.exist(sendEvents[0].msg._msgid);
sendEvents[1].msg.should.equal(messages[2]);
sendEvents[1].destination.should.eql({id:"n4", node: undefined});
sendEvents[1].source.should.eql({id:"n1", node: n1, port: 2})
sendEvents[1].cloneMessage.should.be.true();
should.exist(sendEvents[1].msg._msgid);
sendEvents[2].msg.should.equal(messages[2]);
sendEvents[2].destination.should.eql({id:"n5", node: undefined});
sendEvents[2].source.should.eql({id:"n1", node: n1, port: 2})
sendEvents[2].cloneMessage.should.be.true();
should.exist(sendEvents[2].msg._msgid);
sendEvents[0].msg._msgid.should.eql(sendEvents[1].msg._msgid)
sendEvents[1].msg._msgid.should.eql(sendEvents[2].msg._msgid)
done();
} catch(err) {
done(err);
}
}
};
var n1 = new RedNode({_flow:flow, id:'n1',type:'abc',wires:[['n2'],['n3'],['n4','n5']]});
var n2 = new RedNode({_flow:flow, id:'n2',type:'abc'});
var n3 = new RedNode({_flow:flow, id:'n3',type:'abc'});
var n4 = new RedNode({_flow:flow, id:'n4',type:'abc'});
var n5 = new RedNode({_flow:flow, id:'n5',type:'abc'});
var messages = [
{payload:"hello world"},
null,
@@ -354,48 +547,12 @@ describe('Node', function() {
var rcvdCount = 0;
// first message sent, don't clone
// message uuids should match
n2.on('input',function(msg) {
should.deepEqual(msg,messages[0]);
should.strictEqual(msg,messages[0]);
rcvdCount += 1;
if (rcvdCount == 3) {
done();
}
});
n3.on('input',function(msg) {
should.fail(null,null,"unexpected message");
});
// second message sent, clone
// message uuids wont match since we've cloned
n4.on('input',function(msg) {
msg.payload.should.equal(messages[2].payload);
should.notStrictEqual(msg,messages[2]);
rcvdCount += 1;
if (rcvdCount == 3) {
done();
}
});
// third message sent, clone
// message uuids wont match since we've cloned
n5.on('input',function(msg) {
msg.payload.should.equal(messages[2].payload);
should.notStrictEqual(msg,messages[2]);
rcvdCount += 1;
if (rcvdCount == 3) {
done();
}
});
n1.send(messages);
});
it('emits no messages', function(done) {
var flow = {
handleError: (node,logMessage,msg,reportingNode) => {done(logMessage)},
getNode: (id) => { return {'n1':n1,'n2':n2}[id]},
};
var n1 = new RedNode({_flow:flow,id:'n1',type:'abc',wires:[['n2']]});
@@ -412,105 +569,85 @@ describe('Node', function() {
n1.send();
});
it('emits messages ignoring non-existent nodes', function(done) {
var flow = {
getNode: (id) => { return {'n1':n1,'n2':n2}[id]},
};
var n1 = new RedNode({_flow:flow,id:'n1',type:'abc',wires:[['n9'],['n2']]});
var n2 = new RedNode({_flow:flow,id:'n2',type:'abc'});
// it('emits messages without cloning req or res', function(done) {
// var flow = {
// getNode: (id) => { return {'n1':n1,'n2':n2,'n3':n3}[id]},
// send: (node,dst,msg) => { setImmediate(function() { flow.getNode(dst).receive(msg) })}
// };
// var n1 = new RedNode({_flow:flow,id:'n1',type:'abc',wires:[[['n2'],['n3']]]});
// var n2 = new RedNode({_flow:flow,id:'n2',type:'abc'});
// var n3 = new RedNode({_flow:flow,id:'n3',type:'abc'});
//
// var req = {};
// var res = {};
// var cloned = {};
// var message = {payload: "foo", cloned: cloned, req: req, res: res};
//
// var rcvdCount = 0;
//
// // first message to be sent, so should not be cloned
// n2.on('input',function(msg) {
// should.deepEqual(msg, message);
// msg.cloned.should.be.exactly(message.cloned);
// msg.req.should.be.exactly(message.req);
// msg.res.should.be.exactly(message.res);
// rcvdCount += 1;
// if (rcvdCount == 2) {
// done();
// }
// });
//
// // second message to be sent, so should be cloned
// // message uuids wont match since we've cloned
// n3.on('input',function(msg) {
// msg.payload.should.equal(message.payload);
// msg.cloned.should.not.be.exactly(message.cloned);
// msg.req.should.be.exactly(message.req);
// msg.res.should.be.exactly(message.res);
// rcvdCount += 1;
// if (rcvdCount == 2) {
// done();
// }
// });
//
// n1.send(message);
// });
var messages = [
{payload:"hello world"},
{payload:"hello world again"}
];
// only one message sent, so no copy needed
n2.on('input',function(msg) {
should.deepEqual(msg,messages[1]);
should.strictEqual(msg,messages[1]);
done();
});
n1.send(messages);
});
it('emits messages without cloning req or res', function(done) {
var flow = {
getNode: (id) => { return {'n1':n1,'n2':n2,'n3':n3}[id]},
};
var n1 = new RedNode({_flow:flow,id:'n1',type:'abc',wires:[[['n2'],['n3']]]});
var n2 = new RedNode({_flow:flow,id:'n2',type:'abc'});
var n3 = new RedNode({_flow:flow,id:'n3',type:'abc'});
var req = {};
var res = {};
var cloned = {};
var message = {payload: "foo", cloned: cloned, req: req, res: res};
var rcvdCount = 0;
// first message to be sent, so should not be cloned
n2.on('input',function(msg) {
should.deepEqual(msg, message);
msg.cloned.should.be.exactly(message.cloned);
msg.req.should.be.exactly(message.req);
msg.res.should.be.exactly(message.res);
rcvdCount += 1;
if (rcvdCount == 2) {
done();
}
});
// second message to be sent, so should be cloned
// message uuids wont match since we've cloned
n3.on('input',function(msg) {
msg.payload.should.equal(message.payload);
msg.cloned.should.not.be.exactly(message.cloned);
msg.req.should.be.exactly(message.req);
msg.res.should.be.exactly(message.res);
rcvdCount += 1;
if (rcvdCount == 2) {
done();
}
});
n1.send(message);
});
it("logs the uuid for all messages sent", function(done) {
var logHandler = {
msgIds:[],
messagesSent: 0,
emit: function(event, msg) {
if (msg.event == "node.abc.send" && msg.level == Log.METRIC) {
this.messagesSent++;
this.msgIds.push(msg.msgid);
(typeof msg.msgid).should.not.be.equal("undefined");
}
}
};
Log.addHandler(logHandler);
var flow = {
getNode: (id) => { return {'n1':sender,'n2':receiver1,'n3':receiver2}[id]},
};
var sender = new RedNode({_flow:flow,id:'n1',type:'abc', wires:[['n2', 'n3']]});
var receiver1 = new RedNode({_flow:flow,id:'n2',type:'abc'});
var receiver2 = new RedNode({_flow:flow,id:'n3',type:'abc'});
sender.send({"some": "message"});
setTimeout(function() {
try {
logHandler.messagesSent.should.equal(1);
should.exist(logHandler.msgIds[0])
Log.removeHandler(logHandler);
done();
} catch(err) {
Log.removeHandler(logHandler);
done(err);
}
},50)
})
// it("logs the uuid for all messages sent", function(done) {
// var logHandler = {
// msgIds:[],
// messagesSent: 0,
// emit: function(event, msg) {
// if (msg.event == "node.abc.send" && msg.level == Log.METRIC) {
// this.messagesSent++;
// this.msgIds.push(msg.msgid);
// (typeof msg.msgid).should.not.be.equal("undefined");
// }
// }
// };
//
// Log.addHandler(logHandler);
// var flow = {
// getNode: (id) => { return {'n1':sender,'n2':receiver1,'n3':receiver2}[id]},
// send: (node,dst,msg) => { setImmediate(function() { flow.getNode(dst).receive(msg) })}
// };
//
// var sender = new RedNode({_flow:flow,id:'n1',type:'abc', wires:[['n2', 'n3']]});
// var receiver1 = new RedNode({_flow:flow,id:'n2',type:'abc'});
// var receiver2 = new RedNode({_flow:flow,id:'n3',type:'abc'});
// sender.send({"some": "message"});
// setTimeout(function() {
// try {
// logHandler.messagesSent.should.equal(1);
// should.exist(logHandler.msgIds[0])
// Log.removeHandler(logHandler);
// done();
// } catch(err) {
// Log.removeHandler(logHandler);
// done(err);
// }
// },50)
// })
});

View File

@@ -23,7 +23,7 @@ var inherits = require("util").inherits;
var NR_TEST_UTILS = require("nr-test-utils");
var index = NR_TEST_UTILS.require("@node-red/runtime/lib/nodes/index");
var flows = NR_TEST_UTILS.require("@node-red/runtime/lib/nodes/flows");
var flows = NR_TEST_UTILS.require("@node-red/runtime/lib/flows");
var registry = NR_TEST_UTILS.require("@node-red/registry")
var Node = NR_TEST_UTILS.require("@node-red/runtime/lib/nodes/Node");