From cc88ebd2b9995812f3c7d464d9194739497d22e7 Mon Sep 17 00:00:00 2001 From: Dave Conway-Jones Date: Fri, 17 Nov 2017 17:35:18 +0000 Subject: [PATCH] Let trigger node support per topic mode (#1398) * Let trigger node support per topic mode * ensure trigger node clones repeating message * Add some tests for trigger by topic * test trigger repeat for pass by ref error * trigger test - add missing try/catch to all test with callback * boost trigger node test coverage --- nodes/core/core/89-trigger.html | 8 + nodes/core/core/89-trigger.js | 72 ++--- nodes/core/locales/en-US/messages.json | 3 + test/nodes/core/core/89-trigger_spec.js | 342 +++++++++++++++++++----- 4 files changed, 328 insertions(+), 97 deletions(-) diff --git a/nodes/core/core/89-trigger.html b/nodes/core/core/89-trigger.html index 165e09d8d..c3bb4f2f7 100644 --- a/nodes/core/core/89-trigger.html +++ b/nodes/core/core/89-trigger.html @@ -56,6 +56,13 @@
+
+ + +
@@ -103,6 +110,7 @@ extend: {value:"false"}, units: {value:"ms"}, reset: {value:""}, + bytopic: {value: "all"}, name: {value:""} }, inputs:1, diff --git a/nodes/core/core/89-trigger.js b/nodes/core/core/89-trigger.js index 5314645a0..9a664495a 100644 --- a/nodes/core/core/89-trigger.js +++ b/nodes/core/core/89-trigger.js @@ -19,6 +19,7 @@ module.exports = function(RED) { var mustache = require("mustache"); function TriggerNode(n) { RED.nodes.createNode(this,n); + this.bytopic = n.bytopic || "all"; this.op1 = n.op1 || "1"; this.op2 = n.op2 || "0"; this.op1type = n.op1type || "str"; @@ -47,7 +48,7 @@ module.exports = function(RED) { this.extend = n.extend || "false"; this.units = n.units || "ms"; this.reset = n.reset || ''; - this.duration = parseInt(n.duration); + this.duration = parseFloat(n.duration); if (isNaN(this.duration)) { this.duration = 250; } @@ -65,29 +66,32 @@ module.exports = function(RED) { this.op2Templated = (this.op2type === 'str' && this.op2.indexOf("{{") != -1); if ((this.op1type === "num") && (!isNaN(this.op1))) { this.op1 = Number(this.op1); } if ((this.op2type === "num") && (!isNaN(this.op2))) { this.op2 = Number(this.op2); } - if (this.op1 == "null") { this.op1 = null; } - if (this.op2 == "null") { this.op2 = null; } + //if (this.op1 == "null") { this.op1 = null; } + //if (this.op2 == "null") { this.op2 = null; } //try { this.op1 = JSON.parse(this.op1); } //catch(e) { this.op1 = this.op1; } //try { this.op2 = JSON.parse(this.op2); } //catch(e) { this.op2 = this.op2; } var node = this; - var tout = null; - var m2; + node.topics = {}; + this.on("input", function(msg) { + var topic = msg.topic || "_none"; + if (node.bytopic === "all") { topic = "_none"; } + node.topics[topic] = node.topics[topic] || {}; if (msg.hasOwnProperty("reset") || ((node.reset !== '') && (msg.payload == node.reset)) ) { - if (node.loop === true) { clearInterval(tout); } - else { clearTimeout(tout); } - tout = null; + if (node.loop === true) { clearInterval(node.topics[topic].tout); } + else { clearTimeout(node.topics[topic].tout); } + delete node.topics[topic]; node.status({}); } else { - if (((!tout) && (tout !== 0)) || (node.loop === true)) { - if (node.op2type === "pay" || node.op2type === "payl") { m2 = msg.payload; } - else if (node.op2Templated) { m2 = mustache.render(node.op2,msg); } + if (((!node.topics[topic].tout) && (node.topics[topic].tout !== 0)) || (node.loop === true)) { + if (node.op2type === "pay" || node.op2type === "payl") { node.topics[topic].m2 = RED.util.cloneMessage(msg.payload); } + else if (node.op2Templated) { node.topics[topic].m2 = mustache.render(node.op2,msg); } else if (node.op2type !== "nul") { - m2 = RED.util.evaluateNodeProperty(node.op2,node.op2type,node,msg); + node.topics[topic].m2 = RED.util.evaluateNodeProperty(node.op2,node.op2type,node,msg); } if (node.op1type === "pay") { } @@ -96,58 +100,60 @@ module.exports = function(RED) { msg.payload = RED.util.evaluateNodeProperty(node.op1,node.op1type,node,msg); } - if (node.op1type !== "nul") { node.send(msg); } + if (node.op1type !== "nul") { node.send(RED.util.cloneMessage(msg)); } - if (node.duration === 0) { tout = 0; } + if (node.duration === 0) { node.topics[topic].tout = 0; } else if (node.loop === true) { - if (tout) { clearInterval(tout); } + if (node.topics[topic].tout) { clearInterval(node.topics[topic].tout); } if (node.op1type !== "nul") { var msg2 = RED.util.cloneMessage(msg); - tout = setInterval(function() { node.send(msg2); },node.duration); + node.topics[topic].tout = setInterval(function() { node.send(RED.util.cloneMessage(msg2)); }, node.duration); } } else { - tout = setTimeout(function() { + node.topics[topic].tout = setTimeout(function() { if (node.op2type !== "nul") { var msg2 = RED.util.cloneMessage(msg); if (node.op2type === "flow" || node.op2type === "global") { - m2 = RED.util.evaluateNodeProperty(node.op2,node.op2type,node,msg); + node.topics[topic].m2 = RED.util.evaluateNodeProperty(node.op2,node.op2type,node,msg); } - msg2.payload = m2; + msg2.payload = node.topics[topic].m2; node.send(msg2); } - tout = null; + delete node.topics[topic]; node.status({}); - },node.duration); + }, node.duration); } node.status({fill:"blue",shape:"dot",text:" "}); } else if ((node.extend === "true" || node.extend === true) && (node.duration > 0)) { - if (tout) { clearTimeout(tout); } - if (node.op2type === "payl") { m2 = msg.payload; } - tout = setTimeout(function() { + if (node.topics[topic].tout) { clearTimeout(node.topics[topic].tout); } + if (node.op2type === "payl") { node.topics[topic].m2 = RED.util.cloneMessage(msg.payload); } + node.topics[topic].tout = setTimeout(function() { if (node.op2type !== "nul") { var msg2 = RED.util.cloneMessage(msg); if (node.op2type === "flow" || node.op2type === "global") { - m2 = RED.util.evaluateNodeProperty(node.op2,node.op2type,node,msg); + node.topics[topic].m2 = RED.util.evaluateNodeProperty(node.op2,node.op2type,node,msg); } - msg2.payload = m2; + msg2.payload = node.topics[topic].m2; node.send(msg2); } - tout = null; + delete node.topics[topic]; node.status({}); - },node.duration); + }, node.duration); } else { - if (node.op2type === "payl") { m2 = msg.payload; } + if (node.op2type === "payl") { node.topics[topic].m2 = RED.util.cloneMessage(msg.payload); } } } }); this.on("close", function() { - if (tout) { - if (node.loop === true) { clearInterval(tout); } - else { clearTimeout(tout); } - tout = null; + for (var t in node.topics) { + if (node.topics[t]) { + if (node.loop === true) { clearInterval(node.topics[t].tout); } + else { clearTimeout(node.topics[t].tout); } + delete node.topics[t]; + } } node.status({}); }); diff --git a/nodes/core/locales/en-US/messages.json b/nodes/core/locales/en-US/messages.json index 113ddfdca..6728db7c1 100644 --- a/nodes/core/locales/en-US/messages.json +++ b/nodes/core/locales/en-US/messages.json @@ -270,6 +270,9 @@ "wait-reset": "wait to be reset", "wait-for": "wait for", "wait-loop": "resend it every", + "for": "Handling", + "bytopics": "each msg.topic independently", + "alltopics": "all messages", "duration": { "ms": "Milliseconds", "s": "Seconds", diff --git a/test/nodes/core/core/89-trigger_spec.js b/test/nodes/core/core/89-trigger_spec.js index 85726b385..74ba19d16 100644 --- a/test/nodes/core/core/89-trigger_spec.js +++ b/test/nodes/core/core/89-trigger_spec.js @@ -15,8 +15,10 @@ **/ var should = require("should"); +var sinon = require("sinon"); var helper = require("../../helper.js"); var triggerNode = require("../../../../nodes/core/core/89-trigger.js"); +var RED = require("../../../../red/red.js"); describe('trigger node', function() { @@ -81,14 +83,17 @@ describe('trigger node', function() { var n2 = helper.getNode("n2"); var c = 0; n2.on("input", function(msg) { - if (c === 0) { - msg.should.have.a.property("payload", '1'); - c+=1; - } - else { - msg.should.have.a.property("payload", '0'); - done(); + try { + if (c === 0) { + msg.should.have.a.property("payload", '1'); + c+=1; + } + else { + msg.should.have.a.property("payload", '0'); + done(); + } } + catch(err) { done(err); } }); n1.emit("input", {payload:null}); }); @@ -161,6 +166,155 @@ describe('trigger node', function() { }); }); + it('should handle multiple topics as one if not asked to handle', function(done) { + var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", bytopic:"all", op1:"1", op2:"0", op1type:"num", op2type:"num", duration:"30", wires:[["n2"]] }, + {id:"n2", type:"helper"} ]; + helper.load(triggerNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + var c = 0; + n2.on("input", function(msg) { + try { + c += 1; + if (c === 1) { + msg.should.have.a.property("payload", 1); + msg.should.have.a.property("topic", "A"); + } + else if (c === 2) { + msg.should.have.a.property("payload", 0); + msg.should.have.a.property("topic", "A"); + done(); + } + } catch(err) { + done(err); + } + }); + n1.emit("input", {payload:1,topic:"A"}); + n1.emit("input", {payload:2,topic:"B"}); + n1.emit("input", {payload:3,topic:"C"}); + }); + }); + + it('should handle multiple topics individually if asked to do so', function(done) { + var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", bytopic:"topic", op1:"1", op2:"0", op1type:"num", op2type:"num", duration:"30", wires:[["n2"]] }, + {id:"n2", type:"helper"} ]; + helper.load(triggerNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + var c = 0; + n2.on("input", function(msg) { + try { + c += 1; + if (c === 1) { + msg.should.have.a.property("payload", 1); + msg.should.have.a.property("topic", "A"); + } + else if (c === 2) { + msg.should.have.a.property("payload", 1); + msg.should.have.a.property("topic", "B"); + } + else if (c === 3) { + msg.should.have.a.property("payload", 1); + msg.should.have.a.property("topic", "C"); + } + else if (c === 4) { + msg.should.have.a.property("payload", 0); + msg.should.have.a.property("topic", "A"); + } + else if (c === 5) { + msg.should.have.a.property("payload", 0); + msg.should.have.a.property("topic", "B"); + } + else if (c === 6) { + msg.should.have.a.property("payload", 0); + msg.should.have.a.property("topic", "C"); + done(); + } + } catch(err) { + done(err); + } + }); + n1.emit("input", {payload:1,topic:"A"}); + n1.emit("input", {payload:2,topic:"B"}); + n1.emit("input", {payload:3,topic:"C"}); + }); + }); + + it('should handle multiple topics individually, and extend one, if asked to do so', function(done) { + var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", bytopic:"topic", extend:"true", op1:"1", op2:"0", op1type:"num", op2type:"num", duration:"30", wires:[["n2"]] }, + {id:"n2", type:"helper"} ]; + helper.load(triggerNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + var c = 0; + n2.on("input", function(msg) { + try { + c += 1; + if (c === 1) { + msg.should.have.a.property("payload", 1); + msg.should.have.a.property("topic", "A"); + } + else if (c === 2) { + msg.should.have.a.property("payload", 1); + msg.should.have.a.property("topic", "B"); + } + else if (c === 3) { + msg.should.have.a.property("payload", 1); + msg.should.have.a.property("topic", "C"); + } + else if (c === 4) { + msg.should.have.a.property("payload", 0); + msg.should.have.a.property("topic", "A"); + } + else if (c === 5) { + msg.should.have.a.property("payload", 0); + msg.should.have.a.property("topic", "C"); + } + else if (c === 6) { + msg.should.have.a.property("payload", 0); + msg.should.have.a.property("topic", "B"); + done(); + } + } catch(err) { + done(err); + } + }); + n1.emit("input", {payload:1,topic:"A"}); + n1.emit("input", {payload:2,topic:"B"}); + n1.emit("input", {payload:3,topic:"C"}); + setTimeout( function() { n1.emit("input", {payload:2,topic:"B"})}, 25 ); + }); + }); + + it('should be able to return things from flow and global context variables', function(done) { + var spy = sinon.stub(RED.util, 'evaluateNodeProperty', + function(arg1, arg2, arg3, arg4) { return arg1; } + ); + var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", op1:"foo", op1type:"flow", op2:"bar", op2type:"global", duration:"20", wires:[["n2"]] }, + {id:"n2", type:"helper"} ]; + helper.load(triggerNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + var c = 0; + n2.on("input", function(msg) { + try { + if (c === 0) { + msg.should.have.a.property("payload", "foo"); + c+=1; + } + else { + msg.should.have.a.property("payload", "bar"); + spy.restore(); + done(); + } + } + catch(err) { spy.restore(); done(err); } + }); + n1.emit("input", {payload:null}); + }); + + }); + it('should be able to not output anything on first trigger', function(done) { var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", op1type:"nul", op1:"true",op2:"false",op2type:"val",duration:"30", wires:[["n2"]] }, {id:"n2", type:"helper"} ]; @@ -187,8 +341,11 @@ describe('trigger node', function() { var n2 = helper.getNode("n2"); var c = 0; n2.on("input", function(msg) { - msg.should.have.a.property("payload", true); - c += 1; + try { + msg.should.have.a.property("payload", true); + c += 1; + } + catch(err) { done(err); } }); setTimeout( function() { c.should.equal(1); // should only have had one output. @@ -199,23 +356,30 @@ describe('trigger node', function() { }); it('should be able to extend the delay', function(done) { - var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", extend:"true", op1type:"pay", op1:"false", op2:"true", duration:"100", wires:[["n2"]] }, + var spy = sinon.stub(RED.util, 'evaluateNodeProperty', + function(arg1, arg2, arg3, arg4) { return arg1; } + ); + var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", extend:"true", op1type:"flow", op1:"foo", op2:"bar", op2type:"global", duration:"100", wires:[["n2"]] }, {id:"n2", type:"helper"} ]; helper.load(triggerNode, flow, function() { var n1 = helper.getNode("n1"); var n2 = helper.getNode("n2"); var c = 0; n2.on("input", function(msg) { - if (c === 0) { - msg.should.have.a.property("payload", "Hello"); - c += 1; - } - else { - msg.should.have.a.property("payload", "true"); - //console.log(Date.now() - ss); - (Date.now() - ss).should.be.greaterThan(149); - done(); + try { + if (c === 0) { + msg.should.have.a.property("payload", "foo"); + c += 1; + } + else { + msg.should.have.a.property("payload", "bar"); + //console.log(Date.now() - ss); + (Date.now() - ss).should.be.greaterThan(149); + spy.restore(); + done(); + } } + catch(err) { spy.restore(); done(err); } }); var ss = Date.now(); n1.emit("input", {payload:"Hello"}); @@ -233,16 +397,19 @@ describe('trigger node', function() { var n2 = helper.getNode("n2"); var c = 0; n2.on("input", function(msg) { - if (c === 0) { - msg.should.have.a.property("payload", "Hello"); - c += 1; - } - else { - msg.should.have.a.property("payload", "World"); - //console.log(Date.now() - ss); - (Date.now() - ss).should.be.greaterThan(70); - done(); + try { + if (c === 0) { + msg.should.have.a.property("payload", "Hello"); + c += 1; + } + else { + msg.should.have.a.property("payload", "World"); + //console.log(Date.now() - ss); + (Date.now() - ss).should.be.greaterThan(70); + done(); + } } + catch(err) { done(err); } }); var ss = Date.now(); n1.emit("input", {payload:"Hello"}); @@ -263,15 +430,18 @@ describe('trigger node', function() { var n2 = helper.getNode("n2"); var c = 0; n2.on("input", function(msg) { - if (c === 0) { - msg.should.have.a.property("payload", "Goodbye"); - c += 1; - } - else { - msg.should.have.a.property("payload", "World"); - (Date.now() - ss).should.be.greaterThan(70); - done(); + try { + if (c === 0) { + msg.should.have.a.property("payload", "Goodbye"); + c += 1; + } + else { + msg.should.have.a.property("payload", "World"); + (Date.now() - ss).should.be.greaterThan(70); + done(); + } } + catch(err) { done(err); } }); var ss = Date.now(); n1.emit("input", {payload:"Hello"}); @@ -292,15 +462,18 @@ describe('trigger node', function() { var n2 = helper.getNode("n2"); var c = 0; n2.on("input", function(msg) { - if (c === 0) { - msg.should.have.a.property("payload", "Goodbye"); - c += 1; - } - else { - msg.should.have.a.property("payload", "World"); - (Date.now() - ss).should.be.greaterThan(70); - done(); + try { + if (c === 0) { + msg.should.have.a.property("payload", "Goodbye"); + c += 1; + } + else { + msg.should.have.a.property("payload", "World"); + (Date.now() - ss).should.be.greaterThan(70); + done(); + } } + catch(err) { done(err); } }); var ss = Date.now(); n1.emit("input", {payload:"Hello"}); @@ -321,14 +494,17 @@ describe('trigger node', function() { var n2 = helper.getNode("n2"); var c = 0; n2.on("input", function(msg) { - if (c === 0) { - msg.should.have.a.property("payload", "Hello"); - c+=1; - } - else { - msg.should.have.a.property("payload", "World"); - done(); + try { + if (c === 0) { + msg.should.have.a.property("payload", "Hello"); + c+=1; + } + else { + msg.should.have.a.property("payload", "World"); + done(); + } } + catch(err) { done(err); } }); n1.emit("input", {payload:"Hello",topic:"World"}); }); @@ -342,19 +518,46 @@ describe('trigger node', function() { var n2 = helper.getNode("n2"); var c = 0; n2.on("input", function(msg) { - if (c === 0) { - msg.should.have.a.property("payload", null); - c+=1; - } - else { - msg.should.have.a.property("payload", "World"); - done(); + try { + if (c === 0) { + msg.should.have.a.property("payload", null); + c+=1; + } + else { + msg.should.have.a.property("payload", "World"); + done(); + } } + catch(err) { done(err); } }); n1.emit("input", {payload:"World"}); }); }); + it('should handle string null as null on op2', function(done) { + var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", op1type:"val", op2type:"val", op1:"null", op2:"null", duration:"40", wires:[["n2"]] }, + {id:"n2", type:"helper"} ]; + helper.load(triggerNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + var c = 0; + n2.on("input", function(msg) { + try { + if (c === 0) { + msg.should.have.a.property("payload", null); + c+=1; + } + else { + msg.should.have.a.property("payload", null); + done(); + } + } + catch(err) { done(err); } + }); + n1.emit("input", {payload:"null"}); + }); + }); + it('should be able to set infinite timeout, and clear timeout', function(done) { var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", duration:"0", extend: false, wires:[["n2"]] }, {id:"n2", type:"helper"} ]; @@ -363,8 +566,11 @@ describe('trigger node', function() { var n2 = helper.getNode("n2"); var c = 0; n2.on("input", function(msg) { - c += 1; - msg.should.have.a.property("payload", 1); + try { + c += 1; + msg.should.have.a.property("payload", "1"); + } + catch(err) { done(err); } }); setTimeout( function() { if (c === 2) { done(); } @@ -388,8 +594,11 @@ describe('trigger node', function() { var n2 = helper.getNode("n2"); var c = 0; n2.on("input", function(msg) { - c += 1; - msg.should.have.a.property("payload", 1); + try { + c += 1; + msg.should.have.a.property("payload", "1"); + } + catch(err) { done(err); } }); setTimeout( function() { if (c === 2) { done(); } @@ -406,7 +615,7 @@ describe('trigger node', function() { }); it('should be able to set a repeat, and clear loop by reset', function(done) { - var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", reset:"boo", duration:-25, wires:[["n2"]] }, + var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", reset:"boo", op1:"", op1type:"pay", duration:-25, wires:[["n2"]] }, {id:"n2", type:"helper"} ]; helper.load(triggerNode, flow, function() { var n1 = helper.getNode("n1"); @@ -414,9 +623,14 @@ describe('trigger node', function() { var c = 0; n2.on("input", function(msg) { c += 1; - msg.should.have.a.property("payload", "foo"); + try { + msg.should.have.property('payload','foo'); + msg.payload = "bar"; // try to provoke pass by reference error + } + catch(err) { done(err); } }); n1.emit("input", {payload:"foo"}); // trigger + n1.emit("input", {payload:"foo"}); // trigger setTimeout( function() { n1.emit("input", {reset:true}); // reset },90);