From 81f4e0de56e1e1273612dbbe00059ab6325b208f Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Wed, 16 Jan 2019 16:27:19 +0000 Subject: [PATCH] Refactor Subflow logic into own class --- .../@node-red/runtime/lib/nodes/Node.js | 15 +- .../@node-red/runtime/lib/nodes/flows/Flow.js | 544 +++++++--------- .../runtime/lib/nodes/flows/Subflow.js | 246 ++++++++ .../runtime/lib/nodes/flows/index.js | 98 +-- .../@node-red/runtime/lib/nodes/flows/util.js | 47 +- .../@node-red/runtime/lib/nodes/Node_spec.js | 150 ++--- .../runtime/lib/nodes/flows/Flow_spec.js | 593 ++---------------- .../runtime/lib/nodes/flows/Subflow_spec.js | 460 ++++++++++++++ .../runtime/lib/nodes/flows/index_spec.js | 216 +++---- 9 files changed, 1214 insertions(+), 1155 deletions(-) create mode 100644 packages/node_modules/@node-red/runtime/lib/nodes/flows/Subflow.js create mode 100644 test/unit/@node-red/runtime/lib/nodes/flows/Subflow_spec.js diff --git a/packages/node_modules/@node-red/runtime/lib/nodes/Node.js b/packages/node_modules/@node-red/runtime/lib/nodes/Node.js index 6f2998d06..44dffe22e 100644 --- a/packages/node_modules/@node-red/runtime/lib/nodes/Node.js +++ b/packages/node_modules/@node-red/runtime/lib/nodes/Node.js @@ -35,6 +35,9 @@ function Node(n) { if (n._alias) { this._alias = n._alias; } + if (n._flow) { + this._flow = n._flow; + } this.updateWires(n.wires); } @@ -131,10 +134,12 @@ Node.prototype.send = function(msg) { msg._msgid = redUtil.generateId(); } this.metric("send",msg); - node = flows.get(this._wire); + node = this._flow.getNode(this._wire); /* istanbul ignore else */ if (node) { node.receive(msg); + } else { + console.log("trying to send to a node not on this flow",this._wire); } return; } else { @@ -163,7 +168,7 @@ Node.prototype.send = function(msg) { var k = 0; // for each recipent node of that output for (var j = 0; j < wires.length; j++) { - node = flows.get(wires[j]); // node at end of wire j + node = this._flow.getNode(wires[j]); // node at end of wire j if (node) { // for each msg to send eg. [[m1, m2, ...], ...] for (k = 0; k < msgs.length; k++) { @@ -182,6 +187,8 @@ Node.prototype.send = function(msg) { } } } + } else { + console.log("trying to send to a node not on this flow",this._wire); } } } @@ -251,7 +258,7 @@ Node.prototype.error = function(logMessage,msg) { } var handled = false; if (msg) { - handled = flows.handleError(this,logMessage,msg); + handled = this._flow.handleError(this,logMessage,msg); } if (!handled) { log_helper(this, Log.ERROR, logMessage); @@ -286,6 +293,6 @@ Node.prototype.metric = function(eventname, msg, metricValue) { * status: { fill:"red|green", shape:"dot|ring", text:"blah" } */ Node.prototype.status = function(status) { - flows.handleStatus(this,status); + this._flow.handleStatus(this,status); }; module.exports = Node; diff --git a/packages/node_modules/@node-red/runtime/lib/nodes/flows/Flow.js b/packages/node_modules/@node-red/runtime/lib/nodes/flows/Flow.js index cbe7f5c3a..c46067f66 100644 --- a/packages/node_modules/@node-red/runtime/lib/nodes/flows/Flow.js +++ b/packages/node_modules/@node-red/runtime/lib/nodes/flows/Flow.js @@ -16,33 +16,68 @@ var when = require("when"); var clone = require("clone"); -var typeRegistry = require("@node-red/registry"); +var Subflow; var Log; var redUtil = require("@node-red/util").util; var flowUtil = require("./util"); +var events = require("../../events"); var nodeCloseTimeout = 15000; class Flow { - constructor(global,flow) { - this.global = global; + constructor(parent,globalFlow,flow) { + this.TYPE = 'flow'; + this.parent = parent; + this.global = globalFlow; if (typeof flow === 'undefined') { - this.flow = global; + this.flow = globalFlow; + this.isGlobalFlow = true; } else { this.flow = flow; + this.isGlobalFlow = false; } + this.id = this.flow.id || "global"; this.activeNodes = {}; this.subflowInstanceNodes = {}; - this.catchNodeMap = {}; - this.statusNodeMap = {}; + this.catchNodes = []; + this.statusNodes = []; } + debug(msg) { + Log.log({ + id: this.id||"global", + level: Log.DEBUG, + type:this.TYPE, + msg:msg + }) + } + + log(msg) { + Log.log({ + id: this.id||"global", + level: Log.INFO, + type:this.TYPE, + msg:msg + }) + } + + trace(msg) { + Log.log({ + id: this.id||"global", + level: Log.TRACE, + type:this.TYPE, + msg:msg + }) + } + + start(diff) { + this.trace("start "+this.TYPE); var node; var newNode; var id; - this.catchNodeMap = {}; - this.statusNodeMap = {}; + this.catchNodes = []; + this.statusNodes = []; var configNodes = Object.keys(this.flow.configs); var configNodeAttempts = {}; @@ -69,7 +104,7 @@ class Flow { } } if (readyToCreate) { - newNode = createNode(node.type,node); + newNode = flowUtil.createNode(this,node); if (newNode) { this.activeNodes[id] = newNode; } @@ -91,7 +126,7 @@ class Flow { node = this.flow.nodes[id]; if (!node.subflow) { if (!this.activeNodes[id]) { - newNode = createNode(node.type,node); + newNode = flowUtil.createNode(this,node); if (newNode) { this.activeNodes[id] = newNode; } @@ -100,13 +135,24 @@ class Flow { if (!this.subflowInstanceNodes[id]) { try { var subflowDefinition = this.flow.subflows[node.subflow]||this.global.subflows[node.subflow] - var nodes = createSubflow(subflowDefinition,node,this.flow.subflows,this.global.subflows,this.activeNodes); - this.subflowInstanceNodes[id] = nodes.map(function(n) { return n.id}); - for (var i=0;i 0) { + this.trace("------------------|--------------|-----------------"); + this.trace(" id | type | alias"); + this.trace("------------------|--------------|-----------------"); + } + // Build the map of catch/status nodes. for (id in this.activeNodes) { if (this.activeNodes.hasOwnProperty(id)) { node = this.activeNodes[id]; + this.trace(" "+id.padEnd(16)+" | "+node.type.padEnd(12)+" | "+(node._alias||"")); if (node.type === "catch") { - this.catchNodeMap[node.z] = this.catchNodeMap[node.z] || []; - this.catchNodeMap[node.z].push(node); + this.catchNodes.push(node); } else if (node.type === "status") { - this.statusNodeMap[node.z] = this.statusNodeMap[node.z] || []; - this.statusNodeMap[node.z].push(node); + this.statusNodes.push(node); } } } + if (activeCount > 0) { + this.trace("------------------|--------------|-----------------"); + } + // this.dump(); } stop(stopList, removedList) { return new Promise((resolve,reject) => { + this.trace("stop "+this.TYPE); var i; if (stopList) { - for (i=0;i { subflow.stop() })); + } catch(err) { + node.error(err); + } delete this.subflowInstanceNodes[stopList[i]]; - } - try { - var removed = removedMap[stopList[i]]; - promises.push( - when.promise(function(resolve, reject) { - var start; - var nt = node.type; - var nid = node.id; - var n = node; - when.promise(function(resolve) { - Log.trace("Stopping node "+nt+":"+nid+(removed?" removed":"")); - start = Date.now(); - resolve(n.close(removed)); - }).timeout(nodeCloseTimeout).then(function(){ - var delta = Date.now() - start; - Log.trace("Stopped node "+nt+":"+nid+" ("+delta+"ms)" ); - resolve(delta); - },function(err) { - var delta = Date.now() - start; - n.error(Log._("nodes.flows.stopping-error",{message:err})); - Log.debug(err.stack); - reject(err); - }); - }) - ); - } catch(err) { - node.error(err); + } else { + try { + var removed = removedMap[stopList[i]]; + promises.push(this.stopNode(node,removed)); + } catch(err) { + node.error(err); + } } } } @@ -193,13 +237,46 @@ class Flow { }); } + stopNode(node,removed) { + return when.promise(function(resolve, reject) { + var start; + when.promise(function(resolve) { + Log.trace("Stopping node "+node.type+":"+node.id+(removed?" removed":"")); + start = Date.now(); + resolve(node.close(removed)); + }).timeout(nodeCloseTimeout).then(function(){ + var delta = Date.now() - start; + Log.trace("Stopped node "+node.type+":"+node.id+" ("+delta+"ms)" ); + resolve(delta); + },function(err) { + var delta = Date.now() - start; + node.error(Log._("nodes.flows.stopping-error",{message:err})); + Log.debug(err.stack); + reject(err); + }); + }) + } + update(_global,_flow) { this.global = _global; this.flow = _flow; } getNode(id) { - return this.activeNodes[id]; + // console.log('getNode',id,!!this.activeNodes[id]) + if (!id) { + return undefined; + } + // console.log((new Error().stack).toString().split("\n").slice(1,3).join("\n")) + if ((this.flow.configs && this.flow.configs[id]) || (this.flow.nodes && this.flow.nodes[id])) { + // This is a node owned by this flow, so return whatever we have got + // During a stop/restart, activeNodes could be null for this id + return this.activeNodes[id]; + } else if (this.activeNodes[id]) { + // TEMP: this is a subflow internal node within this flow + return this.activeNodes[id]; + } + return this.parent.getNode(id); } getActiveNodes() { @@ -207,40 +284,40 @@ class Flow { } handleStatus(node,statusMessage) { - var targetStatusNodes = null; - var reportingNode = node; + events.emit("node-status",{ + id: node.id, + status:statusMessage + }); + var handled = false; - while (reportingNode && !handled) { - targetStatusNodes = this.statusNodeMap[reportingNode.z]; - if (targetStatusNodes) { - targetStatusNodes.forEach(function(targetStatusNode) { - if (targetStatusNode.scope && targetStatusNode.scope.indexOf(node.id) === -1) { - return; - } - var message = { - status: { - text: "", - source: { - id: node.id, - type: node.type, - name: node.name - } - } - }; - if (statusMessage.hasOwnProperty("text")) { - message.status.text = statusMessage.text.toString(); - } - targetStatusNode.receive(message); - handled = true; - }); + this.statusNodes.forEach(function(targetStatusNode) { + if (targetStatusNode.scope && targetStatusNode.scope.indexOf(node.id) === -1) { + return; } - if (!handled) { - reportingNode = this.activeNodes[reportingNode.z]; + var message = { + status: { + text: "", + source: { + id: node.id, + type: node.type, + name: node.name + } + } + }; + if (statusMessage.hasOwnProperty("text")) { + message.status.text = statusMessage.text.toString(); } + targetStatusNode.receive(message); + handled = true; + }); + if (!handled) { + // // Nothing in this flow handled the status - pass it to the parent + this.parent.handleStatus(node,statusMessage); } } handleError(node,logMessage,msg) { + // console.log("HE",logMessage); var count = 1; if (msg && msg.hasOwnProperty("error") && msg.error !== null) { if (msg.error.hasOwnProperty("source") && msg.error.source !== null) { @@ -253,268 +330,67 @@ class Flow { } } } - var targetCatchNodes = null; - var throwingNode = node; var handled = false; - while (throwingNode && !handled) { - targetCatchNodes = this.catchNodeMap[throwingNode.z]; - if (targetCatchNodes) { - targetCatchNodes.forEach(function(targetCatchNode) { - if (targetCatchNode.scope && targetCatchNode.scope.indexOf(throwingNode.id) === -1) { - return; - } - var errorMessage; - if (msg) { - errorMessage = redUtil.cloneMessage(msg); - } else { - errorMessage = {}; - } - if (errorMessage.hasOwnProperty("error")) { - errorMessage._error = errorMessage.error; - } - errorMessage.error = { - message: logMessage.toString(), - source: { - id: node.id, - type: node.type, - name: node.name, - count: count - } - }; - if (logMessage.hasOwnProperty('stack')) { - errorMessage.error.stack = logMessage.stack; - } - targetCatchNode.receive(errorMessage); - handled = true; - }); + this.catchNodes.forEach(function(targetCatchNode) { + if (targetCatchNode.scope && targetCatchNode.scope.indexOf(node.id) === -1) { + return; } - if (!handled) { - throwingNode = this.activeNodes[throwingNode.z]; + var errorMessage; + if (msg) { + errorMessage = redUtil.cloneMessage(msg); + } else { + errorMessage = {}; } + if (errorMessage.hasOwnProperty("error")) { + errorMessage._error = errorMessage.error; + } + errorMessage.error = { + message: logMessage.toString(), + source: { + id: node.id, + type: node.type, + name: node.name, + count: count + } + }; + if (logMessage.hasOwnProperty('stack')) { + errorMessage.error.stack = logMessage.stack; + } + targetCatchNode.receive(errorMessage); + handled = true; + }); + if (!handled) { + // Nothing in this flow handled the error - pass it to the parent + handled = this.parent.handleError(node,logMessage); } return handled; } + + dump() { + console.log("==================") + console.log(this.TYPE, this.id); + for (var id in this.activeNodes) { + if (this.activeNodes.hasOwnProperty(id)) { + var node = this.activeNodes[id]; + console.log(" ",id.padEnd(16),node.type) + if (node.wires) { + console.log(" -> ",node.wires) + } + } + } + console.log("==================") + } } -/** - * Create a new instance of a node - * @param {string} type The node type string - * @param {object} config The node configuration object - * @return {Node} The instance of the node - */ -function createNode(type,config) { - var newNode = null; - try { - var nodeTypeConstructor = typeRegistry.get(type); - if (nodeTypeConstructor) { - var conf = clone(config); - delete conf.credentials; - for (var p in conf) { - if (conf.hasOwnProperty(p)) { - flowUtil.mapEnvVarProperties(conf,p); - } - } - try { - newNode = new nodeTypeConstructor(conf); - } catch (err) { - Log.log({ - level: Log.ERROR, - id:conf.id, - type: type, - msg: err - }); - } - } else { - Log.error(Log._("nodes.flow.unknown-type", {type:type})); - } - } catch(err) { - Log.error(err); - } - return newNode; -} - -function createSubflow(sf,sfn,subflows,globalSubflows,activeNodes) { - //console.log("CREATE SUBFLOW",sf.id,sfn.id); - var nodes = []; - var node_map = {}; - var newNodes = []; - var node; - var wires; - var i,j,k; - - var createNodeInSubflow = function(def) { - node = clone(def); - var nid = redUtil.generateId(); - node_map[node.id] = node; - node._alias = node.id; - node.id = nid; - node.z = sfn.id; - newNodes.push(node); - } - - // Clone all of the subflow config node definitions and give them new IDs - for (i in sf.configs) { - if (sf.configs.hasOwnProperty(i)) { - createNodeInSubflow(sf.configs[i]); - } - } - // Clone all of the subflow node definitions and give them new IDs - for (i in sf.nodes) { - if (sf.nodes.hasOwnProperty(i)) { - createNodeInSubflow(sf.nodes[i]); - } - } - - // Look for any catch/status nodes and update their scope ids - // Update all subflow interior wiring to reflect new node IDs - for (i=0;i",nid, "(",node.type,")") + // node_map[node.id] = node; + node._alias = node.id; + node.id = nid; + node.z = subflowInstanceId; + return node; +} + + +/** + * Given an object of {id:nodes} and a map of {old-id:node}, modifiy all + * properties in the nodes object to reference the new node ids. + * This handles: + * - node.wires, + * - node.scope of Catch and Status nodes, + * - node.XYZ for any property where XYZ is recognised as an old property + * @param {[type]} nodes [description] + * @param {[type]} nodeMap [description] + * @return {[type]} [description] + */ +function remapSubflowNodes(nodes,nodeMap) { + for (var id in nodes) { + if (nodes.hasOwnProperty(id)) { + var node = nodes[id]; + if (node.wires) { + var outputs = node.wires; + for (j=0;j false, + handleStatus: () => false +} + + module.exports = { init: init, @@ -773,8 +719,8 @@ module.exports = { get started() { return started }, - handleError: handleError, - handleStatus: handleStatus, + // handleError: handleError, + // handleStatus: handleStatus, checkTypeInUse: checkTypeInUse, diff --git a/packages/node_modules/@node-red/runtime/lib/nodes/flows/util.js b/packages/node_modules/@node-red/runtime/lib/nodes/flows/util.js index 52fcc9138..30531c49a 100644 --- a/packages/node_modules/@node-red/runtime/lib/nodes/flows/util.js +++ b/packages/node_modules/@node-red/runtime/lib/nodes/flows/util.js @@ -15,6 +15,7 @@ **/ var clone = require("clone"); var redUtil = require("@node-red/util").util; +var Log = require("@node-red/util").log; var subflowInstanceRE = /^subflow:(.+)$/; var typeRegistry = require("@node-red/registry"); @@ -427,9 +428,10 @@ module.exports = { // console.log(diff); // for (id in newConfig.allNodes) { // console.log( - // (added[id]?"+":(changed[id]?"!":" "))+(wiringChanged[id]?"w":" ")+(diff.linked.indexOf(id)!==-1?"~":" "), - // id, - // newConfig.allNodes[id].type, + // (added[id]?"a":(changed[id]?"c":" "))+(wiringChanged[id]?"w":" ")+(diff.linked.indexOf(id)!==-1?"l":" "), + // newConfig.allNodes[id].type.padEnd(10), + // id.padEnd(16), + // (newConfig.allNodes[id].z||"").padEnd(16), // newConfig.allNodes[id].name||newConfig.allNodes[id].label||"" // ); // } @@ -443,5 +445,44 @@ module.exports = { // } return diff; + }, + + /** + * Create a new instance of a node + * @param {Flow} flow The containing flow + * @param {object} config The node configuration object + * @return {Node} The instance of the node + */ + createNode: function(flow,config) { + var newNode = null; + var type = config.type; + try { + var nodeTypeConstructor = typeRegistry.get(type); + if (nodeTypeConstructor) { + var conf = clone(config); + delete conf.credentials; + for (var p in conf) { + if (conf.hasOwnProperty(p)) { + mapEnvVarProperties(conf,p); + } + } + try { + conf._flow = flow; + newNode = new nodeTypeConstructor(conf); + } catch (err) { + Log.log({ + level: Log.ERROR, + id:conf.id, + type: type, + msg: err + }); + } + } else { + Log.error(Log._("nodes.flow.unknown-type", {type:type})); + } + } catch(err) { + Log.error(err); + } + return newNode; } } diff --git a/test/unit/@node-red/runtime/lib/nodes/Node_spec.js b/test/unit/@node-red/runtime/lib/nodes/Node_spec.js index 623250967..b90c5dd8c 100644 --- a/test/unit/@node-red/runtime/lib/nodes/Node_spec.js +++ b/test/unit/@node-red/runtime/lib/nodes/Node_spec.js @@ -167,18 +167,17 @@ describe('Node', function() { describe('#send', function() { it('emits a single message', function(done) { - var n1 = new RedNode({id:'n1',type:'abc',wires:[['n2']]}); - var n2 = new RedNode({id:'n2',type:'abc'}); - var flowGet = sinon.stub(flows,"get",function(id) { - return {'n1':n1,'n2':n2}[id]; - }); + 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"}; n2.on('input',function(msg) { // msg equals message, and is not a new copy should.deepEqual(msg,message); should.strictEqual(msg,message); - flowGet.restore(); done(); }); @@ -186,11 +185,11 @@ describe('Node', function() { }); it('emits multiple messages on a single output', function(done) { - var n1 = new RedNode({id:'n1',type:'abc',wires:[['n2']]}); - var n2 = new RedNode({id:'n2',type:'abc'}); - var flowGet = sinon.stub(flows,"get",function(id) { - return {'n1':n1,'n2':n2}[id]; - }); + 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 messages = [ {payload:"hello world"}, @@ -209,7 +208,6 @@ describe('Node', function() { // second msg sent, clone msg.payload.should.equal(messages[rcvdCount].payload); should.notStrictEqual(msg,messages[rcvdCount]); - flowGet.restore(); done(); } }); @@ -217,14 +215,14 @@ describe('Node', function() { }); it('emits messages to multiple outputs', function(done) { - var n1 = new RedNode({id:'n1',type:'abc',wires:[['n2'],['n3'],['n4','n5']]}); - var n2 = new RedNode({id:'n2',type:'abc'}); - var n3 = new RedNode({id:'n3',type:'abc'}); - var n4 = new RedNode({id:'n4',type:'abc'}); - var n5 = new RedNode({id:'n5',type:'abc'}); - var flowGet = sinon.stub(flows,"get",function(id) { - return {'n1':n1,'n2':n2,'n3':n3,'n4':n4,'n5':n5}[id]; - }); + var flow = { + getNode: (id) => { return {'n1':n1,'n2':n2,'n3':n3,'n4':n4,'n5':n5}[id]}, + }; + 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"}, @@ -241,7 +239,6 @@ describe('Node', function() { should.strictEqual(msg,messages[0]); rcvdCount += 1; if (rcvdCount == 3) { - flowGet.restore(); done(); } }); @@ -257,7 +254,6 @@ describe('Node', function() { should.notStrictEqual(msg,messages[2]); rcvdCount += 1; if (rcvdCount == 3) { - flowGet.restore(); done(); } }); @@ -269,7 +265,6 @@ describe('Node', function() { should.notStrictEqual(msg,messages[2]); rcvdCount += 1; if (rcvdCount == 3) { - flowGet.restore(); done(); } }); @@ -278,18 +273,17 @@ describe('Node', function() { }); it('emits no messages', function(done) { - var n1 = new RedNode({id:'n1',type:'abc',wires:[['n2']]}); - var n2 = new RedNode({id:'n2',type:'abc'}); - var flowGet = sinon.stub(flows,"get",function(id) { - return {'n1':n1,'n2':n2}[id]; - }); + 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'}); n2.on('input',function(msg) { should.fail(null,null,"unexpected message"); }); setTimeout(function() { - flowGet.restore(); done(); }, 200); @@ -297,11 +291,11 @@ describe('Node', function() { }); it('emits messages ignoring non-existent nodes', function(done) { - var n1 = new RedNode({id:'n1',type:'abc',wires:[['n9'],['n2']]}); - var n2 = new RedNode({id:'n2',type:'abc'}); - var flowGet = sinon.stub(flows,"get",function(id) { - return {'n1':n1,'n2':n2}[id]; - }); + 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'}); var messages = [ {payload:"hello world"}, @@ -312,7 +306,6 @@ describe('Node', function() { n2.on('input',function(msg) { should.deepEqual(msg,messages[1]); should.strictEqual(msg,messages[1]); - flowGet.restore(); done(); }); @@ -320,12 +313,12 @@ describe('Node', function() { }); it('emits messages without cloning req or res', function(done) { - var n1 = new RedNode({id:'n1',type:'abc',wires:[[['n2'],['n3']]]}); - var n2 = new RedNode({id:'n2',type:'abc'}); - var n3 = new RedNode({id:'n3',type:'abc'}); - var flowGet = sinon.stub(flows,"get",function(id) { - return {'n1':n1,'n2':n2,'n3':n3}[id]; - }); + 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 = {}; @@ -342,7 +335,6 @@ describe('Node', function() { msg.res.should.be.exactly(message.res); rcvdCount += 1; if (rcvdCount == 2) { - flowGet.restore(); done(); } }); @@ -356,7 +348,6 @@ describe('Node', function() { msg.res.should.be.exactly(message.res); rcvdCount += 1; if (rcvdCount == 2) { - flowGet.restore(); done(); } }); @@ -365,26 +356,25 @@ describe('Node', function() { }); it("logs the uuid for all messages sent", function(done) { - var flowGet = sinon.stub(flows,"get",function(id) { - return {'n1':sender,'n2':receiver1,'n3':receiver2}[id]; - }); var logHandler = { messagesSent: 0, emit: function(event, msg) { if (msg.event == "node.abc.send" && msg.level == Log.METRIC) { this.messagesSent++; (typeof msg.msgid).should.not.be.equal("undefined"); - flowGet.restore(); done(); } } }; Log.addHandler(logHandler); + var flow = { + getNode: (id) => { return {'n1':sender,'n2':receiver1,'n3':receiver2}[id]}, + }; - var sender = new RedNode({id:'n1',type:'abc', wires:[['n2', 'n3']]}); - var receiver1 = new RedNode({id:'n2',type:'abc'}); - var receiver2 = new RedNode({id:'n3',type:'abc'}); + 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"}); }) }); @@ -434,50 +424,35 @@ describe('Node', function() { describe('#error', function() { it('handles a null error message', function(done) { - var n = new RedNode({id:'123',type:'abc',z:'789'}); - var loginfo = {}; - sinon.stub(Log, 'log', function(msg) { - loginfo = msg; - }); - sinon.stub(flows,"handleError", function(node,message,msg) { - }); - + var flow = { + handleError: sinon.stub() + } + var n = new RedNode({_flow:flow, id:'123',type:'abc',z:'789'}); var message = {a:1}; - n.error(null,message); - should.deepEqual({level:Log.ERROR, id:n.id, type:n.type, msg:"",z:'789'}, loginfo); - flows.handleError.called.should.be.true(); - flows.handleError.args[0][0].should.eql(n); - flows.handleError.args[0][1].should.eql(""); - flows.handleError.args[0][2].should.eql(message); + flow.handleError.called.should.be.true(); + flow.handleError.args[0][0].should.eql(n); + flow.handleError.args[0][1].should.eql(""); + flow.handleError.args[0][2].should.eql(message); - Log.log.restore(); - flows.handleError.restore(); done(); }); it('produces an error message', function(done) { - var n = new RedNode({id:'123',type:'abc',z:'789'}); - var loginfo = {}; - sinon.stub(Log, 'log', function(msg) { - loginfo = msg; - }); - sinon.stub(flows,"handleError", function(node,message,msg) { - }); - + var flow = { + handleError: sinon.stub() + } + var n = new RedNode({_flow:flow, id:'123',type:'abc',z:'789'}); var message = {a:2}; n.error("This is an error",message); - should.deepEqual({level:Log.ERROR, id:n.id, type:n.type, msg:"This is an error",z:'789'}, loginfo); - flows.handleError.called.should.be.true(); - flows.handleError.args[0][0].should.eql(n); - flows.handleError.args[0][1].should.eql("This is an error"); - flows.handleError.args[0][2].should.eql(message); + flow.handleError.called.should.be.true(); + flow.handleError.args[0][0].should.eql(n); + flow.handleError.args[0][1].should.eql("This is an error"); + flow.handleError.args[0][2].should.eql(message); - Log.log.restore(); - flows.handleError.restore(); done(); }); @@ -528,8 +503,10 @@ describe('Node', function() { describe('#status', function() { it('publishes status', function(done) { - sinon.stub(flows,"handleStatus", function(node,message,msg) {}); - var n = new RedNode({id:'123',type:'abc'}); + var flow = { + handleStatus: sinon.stub() + } + var n = new RedNode({_flow:flow,id:'123',type:'abc'}); var status = {fill:"green",shape:"dot",text:"connected"}; var topic; var message; @@ -537,10 +514,9 @@ describe('Node', function() { n.status(status); - flows.handleStatus.called.should.be.true(); - flows.handleStatus.args[0][0].should.eql(n); - flows.handleStatus.args[0][1].should.eql(status); - flows.handleStatus.restore(); + flow.handleStatus.called.should.be.true(); + flow.handleStatus.args[0][0].should.eql(n); + flow.handleStatus.args[0][1].should.eql(status); done(); }); }); diff --git a/test/unit/@node-red/runtime/lib/nodes/flows/Flow_spec.js b/test/unit/@node-red/runtime/lib/nodes/flows/Flow_spec.js index ca15dfd64..06747c628 100644 --- a/test/unit/@node-red/runtime/lib/nodes/flows/Flow_spec.js +++ b/test/unit/@node-red/runtime/lib/nodes/flows/Flow_spec.js @@ -31,7 +31,6 @@ var typeRegistry = NR_TEST_UTILS.require("@node-red/registry"); describe('Flow', function() { var getType; - var getNode; var stoppedNodes = {}; var currentNodes = {}; @@ -44,11 +43,11 @@ describe('Flow', function() { rewiredNodes = {}; createCount = 0; Flow.init({settings:{},log:{ - log: sinon.stub(), - debug: sinon.stub(), - trace: sinon.stub(), - warn: sinon.stub(), - info: sinon.stub(), + log: sinon.stub(), // function() { console.log("l",[...arguments].map(a => JSON.stringify(a)).join(" ")) },// + debug: sinon.stub(), // function() { console.log("d",[...arguments].map(a => JSON.stringify(a)).join(" ")) },//sinon.stub(), + trace: sinon.stub(), // function() { console.log("t",[...arguments].map(a => JSON.stringify(a)).join(" ")) },//sinon.stub(), + warn: sinon.stub(), // function() { console.log("w",[...arguments].map(a => JSON.stringify(a)).join(" ")) },//sinon.stub(), + info: sinon.stub(), // function() { console.log("i",[...arguments].map(a => JSON.stringify(a)).join(" ")) },//sinon.stub(), metric: sinon.stub(), _: function() { return "abc"} }}); @@ -64,6 +63,7 @@ describe('Flow', function() { this.stopped = false; currentNodes[node.id] = node; this.on('input',function(msg) { + // console.log(this.id,msg.payload); node.handled++; node.send(msg); }); @@ -81,6 +81,34 @@ describe('Flow', function() { } util.inherits(TestNode,Node); + var TestErrorNode = function(n) { + Node.call(this,n); + this._index = createCount++; + this.scope = n.scope; + this.name = n.name; + var node = this; + this.foo = n.foo; + this.handled = 0; + this.stopped = false; + currentNodes[node.id] = node; + this.on('input',function(msg) { + node.handled++; + node.error("test error",msg); + }); + this.on('close',function() { + node.stopped = true; + stoppedNodes[node.id] = node; + delete currentNodes[node.id]; + }); + this.__updateWires = this.updateWires; + this.updateWires = function(newWires) { + rewiredNodes[node.id] = node; + node.newWires = newWires; + node.__updateWires(newWires); + }; + } + util.inherits(TestErrorNode,Node); + var TestAsyncNode = function(n) { Node.call(this,n); var node = this; @@ -111,26 +139,21 @@ describe('Flow', function() { getType = sinon.stub(typeRegistry,"get",function(type) { if (type=="test") { return TestNode; + } else if (type=="testError"){ + return TestErrorNode; } else { return TestAsyncNode; } }); - getNode = sinon.stub(flows,"get",function(id) { - return currentNodes[id]; - }); - }); after(function() { getType.restore(); - getNode.restore(); }); - - describe('#constructor',function() { it('called with an empty flow',function() { var config = flowUtils.parseConfig([]); - var flow = Flow.create(config); + var flow = Flow.create({},config); var nodeCount = 0; Object.keys(flow.getActiveNodes()).length.should.equal(0); @@ -145,7 +168,7 @@ describe('Flow', function() { {id:"3",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]}, {id:"4",z:"t1",type:"test",foo:"a"} ]); - var flow = Flow.create(config,config.flows["t1"]); + var flow = Flow.create({},config,config.flows["t1"]); flow.start(); Object.keys(flow.getActiveNodes()).should.have.length(4); @@ -188,7 +211,6 @@ describe('Flow', function() { }); }); - it("instantiates config nodes in the right order",function(done) { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, @@ -198,7 +220,7 @@ describe('Flow', function() { {id:"4",z:"t1",type:"test",foo:"5"}, // This node depends on #5 {id:"5",z:"t1",type:"test"} ]); - var flow = Flow.create(config,config.flows["t1"]); + var flow = Flow.create({},config,config.flows["t1"]); flow.start(); Object.keys(flow.getActiveNodes()).should.have.length(5); @@ -238,130 +260,12 @@ describe('Flow', function() { {id:"node1",z:"t1",type:"test",foo:"node2"}, // This node depends on #5 {id:"node2",z:"t1",type:"test",foo:"node1"} ]); - var flow = Flow.create(config,config.flows["t1"]); + var flow = Flow.create({},config,config.flows["t1"]); /*jshint immed: false */ (function(){ flow.start(); }).should.throw("Circular config node dependency detected: node1"); }); - it("instantiates a subflow and stops it",function(done) { - 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:"subflow:sf1",wires:["3","4"]}, - {id:"3",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]}, - {id:"4",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]}, - {id:"sf1",type:"subflow","name":"Subflow 2","info":"", - "in":[{"wires":[{"id":"sf1-1"}]}],"out":[{"wires":[{"id":"sf1-2","port":0}]},{"wires":[{"id":"sf1","port":0}]}]}, - {id:"sf1-1",type:"test","z":"sf1",x:166,y:99,"wires":[["sf1-2"]]}, - {id:"sf1-2",type:"test","z":"sf1",foo:"sf1-cn",x:166,y:99,"wires":[[]]}, - {id:"sf1-cn",type:"test","z":"sf1"} - ]); - var flow = Flow.create(config,config.flows["t1"]); - - getNode.restore(); - getNode = sinon.stub(flows,"get",function(id) { - return flow.getNode(id); - }); - - flow.start(); - - var activeNodes = flow.getActiveNodes(); - Object.keys(activeNodes).should.have.length(7); - var sfInstanceId = Object.keys(activeNodes)[5]; - var sfInstanceId2 = Object.keys(activeNodes)[6]; - var sfConfigId = Object.keys(activeNodes)[4]; - - flow.getNode('1').should.have.a.property('id','1'); - flow.getNode('2').should.have.a.property('id','2'); - flow.getNode('3').should.have.a.property('id','3'); - flow.getNode('4').should.have.a.property('id','4'); - flow.getNode(sfInstanceId).should.have.a.property('id',sfInstanceId); - flow.getNode(sfInstanceId2).should.have.a.property('id',sfInstanceId2); - flow.getNode(sfConfigId).should.have.a.property('id',sfConfigId); - - flow.getNode(sfInstanceId2).should.have.a.property('foo',sfConfigId); - - currentNodes.should.have.a.property("1"); - currentNodes.should.not.have.a.property("2"); - currentNodes.should.have.a.property("3"); - currentNodes.should.have.a.property("4"); - currentNodes.should.have.a.property(sfInstanceId); - currentNodes.should.have.a.property(sfInstanceId2); - currentNodes.should.have.a.property(sfConfigId); - - currentNodes["1"].should.have.a.property("handled",0); - currentNodes["3"].should.have.a.property("handled",0); - currentNodes["4"].should.have.a.property("handled",0); - currentNodes[sfInstanceId].should.have.a.property("handled",0); - currentNodes[sfInstanceId2].should.have.a.property("handled",0); - - currentNodes["1"].receive({payload:"test"}); - - currentNodes["1"].should.have.a.property("handled",1); - currentNodes[sfInstanceId].should.have.a.property("handled",1); - currentNodes[sfInstanceId2].should.have.a.property("handled",1); - currentNodes["3"].should.have.a.property("handled",1); - currentNodes["4"].should.have.a.property("handled",1); - - - - flow.stop().then(function() { - currentNodes.should.not.have.a.property("1"); - currentNodes.should.not.have.a.property("3"); - currentNodes.should.not.have.a.property("4"); - currentNodes.should.not.have.a.property(sfInstanceId); - currentNodes.should.not.have.a.property(sfInstanceId2); - currentNodes.should.not.have.a.property(sfConfigId); - stoppedNodes.should.have.a.property("1"); - stoppedNodes.should.have.a.property("3"); - stoppedNodes.should.have.a.property("4"); - stoppedNodes.should.have.a.property(sfInstanceId); - stoppedNodes.should.have.a.property(sfInstanceId2); - stoppedNodes.should.have.a.property(sfConfigId); - done(); - }); - }); - - it("instantiates a subflow inside a subflow and stops it",function(done) { - 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:"subflow:sf1",wires:["3","4"]}, - {id:"3",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]}, - {id:"4",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]}, - {id:"sf1",type:"subflow","name":"Subflow 1","info":"", - "in":[{"wires":[{"id":"sf1-1"}]}],"out":[{"wires":[{"id":"sf1-2","port":0}]}]}, - {id:"sf2",type:"subflow","name":"Subflow 2","info":"", - "in":[{wires:[]}],"out":[{"wires":[{"id":"sf2","port":0}]}]}, - {id:"sf1-1",type:"test","z":"sf1",x:166,y:99,"wires":[["sf1-2"]]}, - {id:"sf1-2",type:"subflow:sf2","z":"sf1",x:166,y:99,"wires":[[]]} - - ]); - var flow = Flow.create(config,config.flows["t1"]); - - getNode.restore(); - getNode = sinon.stub(flows,"get",function(id) { - return flow.getNode(id); - }); - - flow.start(); - - currentNodes["1"].should.have.a.property("handled",0); - currentNodes["3"].should.have.a.property("handled",0); - - currentNodes["1"].receive({payload:"test"}); - - currentNodes["1"].should.have.a.property("handled",1); - currentNodes["3"].should.have.a.property("handled",1); - - - - flow.stop().then(function() { - Object.keys(currentNodes).should.have.length(0); - done(); - }); - }); it("rewires nodes specified by diff",function(done) { var config = flowUtils.parseConfig([ @@ -371,7 +275,7 @@ describe('Flow', function() { {id:"3",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]} ]); - var flow = Flow.create(config,config.flows["t1"]); + var flow = Flow.create({},config,config.flows["t1"]); createCount.should.equal(0); flow.start(); //TODO: use update to pass in new wiring and verify the change @@ -382,72 +286,6 @@ describe('Flow', function() { done(); }); - it("rewires a subflow node on update/start",function(done){ - - var rawConfig = [ - {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:"subflow:sf1",wires:["3"]}, - {id:"3",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]}, - {id:"4",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]}, - {id:"sf1",type:"subflow","name":"Subflow 2","info":"", - "in":[{"wires":[{"id":"sf1-1"}]}],"out":[{"wires":[{"id":"sf1-2","port":0}]}]}, - {id:"sf1-1",type:"test1","z":"sf1",x:166,y:99,"wires":[["sf1-2"]]}, - {id:"sf1-2",type:"test2","z":"sf1",x:166,y:99,"wires":[[]]} - ]; - - var config = flowUtils.parseConfig(clone(rawConfig)); - - rawConfig[2].wires = [["4"]]; - - var newConfig = flowUtils.parseConfig(rawConfig); - var diff = flowUtils.diffConfigs(config,newConfig); - var flow = Flow.create(config,config.flows["t1"]); - - getNode.restore(); - getNode = sinon.stub(flows,"get",function(id) { - return flow.getNode(id); - }); - - flow.start(); - - var activeNodes = flow.getActiveNodes(); - Object.keys(activeNodes).should.have.length(6); - var sfInstanceId = Object.keys(activeNodes)[4]; - var sfInstanceId2 = Object.keys(activeNodes)[5]; - - currentNodes["1"].should.have.a.property("handled",0); - currentNodes["3"].should.have.a.property("handled",0); - currentNodes["4"].should.have.a.property("handled",0); - - currentNodes["1"].receive({payload:"test"}); - - currentNodes["1"].should.have.a.property("handled",1); - currentNodes[sfInstanceId].should.have.a.property("handled",1); - currentNodes[sfInstanceId2].should.have.a.property("handled",1); - currentNodes["3"].should.have.a.property("handled",1); - currentNodes["4"].should.have.a.property("handled",0); - - flow.update(newConfig,newConfig.flows["t1"]); - flow.start(diff) - - currentNodes["1"].receive({payload:"test2"}); - - currentNodes["1"].should.have.a.property("handled",2); - currentNodes[sfInstanceId].should.have.a.property("handled",2); - currentNodes[sfInstanceId2].should.have.a.property("handled",2); - currentNodes["3"].should.have.a.property("handled",1); - currentNodes["4"].should.have.a.property("handled",1); - - - flow.stop().then(function() { - done(); - }); - - - }); - - it("instantiates a node with environment variable property values",function(done) { after(function() { delete process.env.NODE_RED_TEST_VALUE; @@ -462,7 +300,7 @@ describe('Flow', function() { {id:"5",x:10,y:10,z:"t1",type:"test",foo:"$(NODE_RED_TEST_VALUE_NONE)",wires:[]}, {id:"6",x:10,y:10,z:"t1",type:"test",foo:["$(NODE_RED_TEST_VALUE)"],wires:[]} ]); - var flow = Flow.create(config,config.flows["t1"]); + var flow = Flow.create({},config,config.flows["t1"]); flow.start(); var activeNodes = flow.getActiveNodes(); @@ -492,7 +330,7 @@ describe('Flow', function() { {id:"2",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["3"]}, {id:"3",x:10,y:10,z:"t1",type:"asyncTest",foo:"a",wires:[]} ]); - var flow = Flow.create(config,config.flows["t1"]); + var flow = Flow.create({},config,config.flows["t1"]); flow.start(); @@ -518,7 +356,7 @@ describe('Flow', function() { {id:"2",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["3"]}, {id:"3",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]} ]); - var flow = Flow.create(config,config.flows["t1"]); + var flow = Flow.create({},config,config.flows["t1"]); flow.start(); currentNodes.should.have.a.property("1"); @@ -536,35 +374,6 @@ describe('Flow', function() { }); }); - it("stops subflow instance nodes",function(done) { - 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:"subflow:sf1",wires:["3"]}, - {id:"3",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]}, - {id:"sf1",type:"subflow","name":"Subflow 2","info":"", - "in":[{"wires":[{"id":"sf1-1"}]}],"out":[{"wires":[{"id":"sf1-1","port":0}]}]}, - {id:"sf1-1",type:"test","z":"sf1",x:166,y:99,"wires":[[]]} - ]); - var flow = Flow.create(config,config.flows["t1"]); - - getNode.restore(); - getNode = sinon.stub(flows,"get",function(id) { - return flow.getNode(id); - }); - - flow.start(); - - var activeNodes = flow.getActiveNodes(); - Object.keys(activeNodes).should.have.length(4); - var sfInstanceId = Object.keys(activeNodes)[3]; - flow.stop(["2"]).then(function() { - currentNodes.should.not.have.a.property(sfInstanceId); - stoppedNodes.should.have.a.property(sfInstanceId); - done(); - }); - }); - it("Times out a node that fails to close", function(done) { Flow.init({settings:{nodeCloseTimeout:50},log:{ log: sinon.stub(), @@ -581,7 +390,7 @@ describe('Flow', function() { {id:"2",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["3"]}, {id:"3",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]} ]); - var flow = Flow.create(config,config.flows["t1"]); + var flow = Flow.create({},config,config.flows["t1"]); flow.start(); currentNodes.should.have.a.property("1"); @@ -615,12 +424,7 @@ describe('Flow', function() { {id:"sn",x:10,y:10,z:"t1",type:"status",foo:"a",wires:[]}, {id:"sn2",x:10,y:10,z:"t1",type:"status",foo:"a",wires:[]} ]); - var flow = Flow.create(config,config.flows["t1"]); - - getNode.restore(); - getNode = sinon.stub(flows,"get",function(id) { - return flow.getNode(id); - }); + var flow = Flow.create({},config,config.flows["t1"]); flow.start(); @@ -664,12 +468,7 @@ describe('Flow', function() { {id:"sn",x:10,y:10,z:"t1",type:"status",scope:["2"],foo:"a",wires:[]}, {id:"sn2",x:10,y:10,z:"t1",type:"status",scope:["1"],foo:"a",wires:[]} ]); - var flow = Flow.create(config,config.flows["t1"]); - - getNode.restore(); - getNode = sinon.stub(flows,"get",function(id) { - return flow.getNode(id); - }); + var flow = Flow.create({},config,config.flows["t1"]); flow.start(); @@ -696,147 +495,6 @@ describe('Flow', function() { }); }); - it("passes a status event to the adjacent status node in subflow",function(done) { - var config = flowUtils.parseConfig([ - {id:"t1",type:"tab"}, - {id:"1",x:10,y:10,z:"t1",type:"test",name:"a",wires:["2"]}, - {id:"2",x:10,y:10,z:"t1",type:"subflow:sf1",wires:["3"]}, - {id:"3",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]}, - {id:"sf1",type:"subflow","name":"Subflow 2","info":"", - "in":[{"wires":[{"id":"sf1-1"}]}],"out":[{"wires":[{"id":"sf1-1","port":0}]}]}, - {id:"sf1-1",type:"test2","z":"sf1",x:166,y:99,"wires":[[]]}, - {id:"sf1-sn",x:10,y:10,z:"sf1",type:"status",foo:"a",wires:[]} - ]); - var flow = Flow.create(config,config.flows["t1"]); - - getNode.restore(); - getNode = sinon.stub(flows,"get",function(id) { - return flow.getNode(id); - }); - - flow.start(); - - var activeNodes = flow.getActiveNodes(); - var sfInstanceId = Object.keys(activeNodes)[3]; - var statusInstanceId = Object.keys(activeNodes)[4]; - - flow.handleStatus(activeNodes[sfInstanceId],{text:"my-status"}); - - currentNodes[statusInstanceId].should.have.a.property("handled",1); - var statusMessage = currentNodes[statusInstanceId].messages[0]; - - statusMessage.should.have.a.property("status"); - statusMessage.status.should.have.a.property("text","my-status"); - statusMessage.status.should.have.a.property("source"); - statusMessage.status.source.should.have.a.property("id",sfInstanceId); - statusMessage.status.source.should.have.a.property("type","test2"); - statusMessage.status.source.should.have.a.property("name",undefined); - flow.stop().then(function() { - - done(); - }); - }); - it("passes a status event to the multiple adjacent status nodes in subflow",function(done) { - var config = flowUtils.parseConfig([ - {id:"t1",type:"tab"}, - {id:"1",x:10,y:10,z:"t1",type:"test",name:"a",wires:["2"]}, - {id:"2",x:10,y:10,z:"t1",type:"subflow:sf1",wires:["3"]}, - {id:"3",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]}, - {id:"4",x:10,y:10,z:"t1",type:"status",foo:"a",wires:[]}, - {id:"sf1",type:"subflow","name":"Subflow 2","info":"", - "in":[{"wires":[{"id":"sf1-1"}]}],"out":[{"wires":[{"id":"sf1-1","port":0}]}]}, - {id:"sf1-1",type:"test2","z":"sf1",x:166,y:99,"wires":[[]]}, - {id:"sf1-sn",x:10,y:10,z:"sf1",type:"status",foo:"a",wires:[]}, - {id:"sf1-sn2",x:10,y:10,z:"sf1",type:"status",scope:["none"],wires:[]}, - {id:"sf1-sn3",x:10,y:10,z:"sf1",type:"status",scope:["sf1-1"],wires:[]} - ]); - var flow = Flow.create(config,config.flows["t1"]); - - getNode.restore(); - getNode = sinon.stub(flows,"get",function(id) { - return flow.getNode(id); - }); - - flow.start(); - - var activeNodes = flow.getActiveNodes(); - - var sfInstanceId = Object.keys(activeNodes)[4]; - var statusInstanceId = Object.keys(activeNodes)[5]; - var statusInstanceId2 = Object.keys(activeNodes)[6]; - var statusInstanceId3 = Object.keys(activeNodes)[7]; - - flow.handleStatus(activeNodes[sfInstanceId],{text:"my-status"}); - - currentNodes[statusInstanceId].should.have.a.property("handled",1); - var statusMessage = currentNodes[statusInstanceId].messages[0]; - - statusMessage.should.have.a.property("status"); - statusMessage.status.should.have.a.property("text","my-status"); - statusMessage.status.should.have.a.property("source"); - statusMessage.status.source.should.have.a.property("id",sfInstanceId); - statusMessage.status.source.should.have.a.property("type","test2"); - statusMessage.status.source.should.have.a.property("name",undefined); - - activeNodes["4"].should.have.a.property("handled",0); - - currentNodes[statusInstanceId2].should.have.a.property("handled",0); - - currentNodes[statusInstanceId3].should.have.a.property("handled",1); - statusMessage = currentNodes[statusInstanceId3].messages[0]; - - statusMessage.should.have.a.property("status"); - statusMessage.status.should.have.a.property("text","my-status"); - statusMessage.status.should.have.a.property("source"); - statusMessage.status.source.should.have.a.property("id",sfInstanceId); - statusMessage.status.source.should.have.a.property("type","test2"); - statusMessage.status.source.should.have.a.property("name",undefined); - - flow.stop().then(function() { - - done(); - }); - }); - it("passes a status event to the subflow's parent tab status node",function(done) { - var config = flowUtils.parseConfig([ - {id:"t1",type:"tab"}, - {id:"1",x:10,y:10,z:"t1",type:"test",name:"a",wires:["2"]}, - {id:"2",x:10,y:10,z:"t1",type:"subflow:sf1",wires:["3"]}, - {id:"3",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]}, - {id:"sf1",type:"subflow","name":"Subflow 2","info":"", - "in":[{"wires":[{"id":"sf1-1"}]}],"out":[{"wires":[{"id":"sf1-1","port":0}]}]}, - {id:"sf1-1",type:"test2","z":"sf1",x:166,y:99,"wires":[[]]}, - {id:"sn",x:10,y:10,z:"t1",type:"status",foo:"a",wires:[]} - ]); - var flow = Flow.create(config,config.flows["t1"]); - - getNode.restore(); - getNode = sinon.stub(flows,"get",function(id) { - return flow.getNode(id); - }); - - flow.start(); - - var activeNodes = flow.getActiveNodes(); - var sfInstanceId = Object.keys(activeNodes)[3]; - - flow.handleStatus(activeNodes[sfInstanceId],{text:"my-status"}); - - currentNodes["sn"].should.have.a.property("handled",1); - var statusMessage = currentNodes["sn"].messages[0]; - - statusMessage.should.have.a.property("status"); - statusMessage.status.should.have.a.property("text","my-status"); - statusMessage.status.should.have.a.property("source"); - statusMessage.status.source.should.have.a.property("id",sfInstanceId); - statusMessage.status.source.should.have.a.property("type","test2"); - statusMessage.status.source.should.have.a.property("name",undefined); - - flow.stop().then(function() { - - done(); - }); - }); }); @@ -850,12 +508,7 @@ describe('Flow', function() { {id:"sn",x:10,y:10,z:"t1",type:"catch",foo:"a",wires:[]}, {id:"sn2",x:10,y:10,z:"t1",type:"catch",foo:"a",wires:[]} ]); - var flow = Flow.create(config,config.flows["t1"]); - - getNode.restore(); - getNode = sinon.stub(flows,"get",function(id) { - return flow.getNode(id); - }); + var flow = Flow.create({},config,config.flows["t1"]); flow.start(); @@ -899,12 +552,7 @@ describe('Flow', function() { {id:"sn",x:10,y:10,z:"t1",type:"catch",scope:["2"],foo:"a",wires:[]}, {id:"sn2",x:10,y:10,z:"t1",type:"catch",scope:["1"],foo:"a",wires:[]} ]); - var flow = Flow.create(config,config.flows["t1"]); - - getNode.restore(); - getNode = sinon.stub(flows,"get",function(id) { - return flow.getNode(id); - }); + var flow = Flow.create({},config,config.flows["t1"]); flow.start(); @@ -930,143 +578,7 @@ describe('Flow', function() { }); }); - it("passes an error event to the adjacent catch node in subflow",function(done) { - var config = flowUtils.parseConfig([ - {id:"t1",type:"tab"}, - {id:"1",x:10,y:10,z:"t1",type:"test",name:"a",wires:["2"]}, - {id:"2",x:10,y:10,z:"t1",type:"subflow:sf1",wires:["3"]}, - {id:"3",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]}, - {id:"sf1",type:"subflow","name":"Subflow 2","info":"", - "in":[{"wires":[{"id":"sf1-1"}]}],"out":[{"wires":[{"id":"sf1-1","port":0}]}]}, - {id:"sf1-1",type:"test2","z":"sf1",x:166,y:99,"wires":[[]]}, - {id:"sf1-sn",x:10,y:10,z:"sf1",type:"catch",foo:"a",wires:[]} - ]); - var flow = Flow.create(config,config.flows["t1"]); - getNode.restore(); - getNode = sinon.stub(flows,"get",function(id) { - return flow.getNode(id); - }); - - flow.start(); - - var activeNodes = flow.getActiveNodes(); - var sfInstanceId = Object.keys(activeNodes)[3]; - var catchInstanceId = Object.keys(activeNodes)[4]; - - flow.handleError(activeNodes[sfInstanceId],"my-error",{a:"foo"}); - - currentNodes[catchInstanceId].should.have.a.property("handled",1); - var statusMessage = currentNodes[catchInstanceId].messages[0]; - - statusMessage.should.have.a.property("error"); - statusMessage.error.should.have.a.property("message","my-error"); - statusMessage.error.should.have.a.property("source"); - statusMessage.error.source.should.have.a.property("id",sfInstanceId); - statusMessage.error.source.should.have.a.property("type","test2"); - statusMessage.error.source.should.have.a.property("name",undefined); - - flow.stop().then(function() { - done(); - }); - }); - - it("passes an error event to the multiple adjacent catch nodes in subflow",function(done) { - var config = flowUtils.parseConfig([ - {id:"t1",type:"tab"}, - {id:"1",x:10,y:10,z:"t1",type:"test",name:"a",wires:["2"]}, - {id:"2",x:10,y:10,z:"t1",type:"subflow:sf1",wires:["3"]}, - {id:"3",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]}, - {id:"sf1",type:"subflow","name":"Subflow 2","info":"", - "in":[{"wires":[{"id":"sf1-1"}]}],"out":[{"wires":[{"id":"sf1-1","port":0}]}]}, - {id:"sf1-1",type:"test2","z":"sf1",x:166,y:99,"wires":[[]]}, - {id:"sf1-sn",x:10,y:10,z:"sf1",type:"catch",foo:"a",wires:[]}, - {id:"sf1-sn2",x:10,y:10,z:"sf1",type:"catch",scope:["none"],wires:[]}, - {id:"sf1-sn3",x:10,y:10,z:"sf1",type:"catch",scope:["sf1-1"],wires:[]} - ]); - var flow = Flow.create(config,config.flows["t1"]); - - getNode.restore(); - getNode = sinon.stub(flows,"get",function(id) { - return flow.getNode(id); - }); - - flow.start(); - - var activeNodes = flow.getActiveNodes(); - var sfInstanceId = Object.keys(activeNodes)[3]; - var catchInstanceId = Object.keys(activeNodes)[4]; - var catchInstanceId2 = Object.keys(activeNodes)[5]; - var catchInstanceId3 = Object.keys(activeNodes)[6]; - - flow.handleError(activeNodes[sfInstanceId],"my-error",{a:"foo"}); - - currentNodes[catchInstanceId].should.have.a.property("handled",1); - var statusMessage = currentNodes[catchInstanceId].messages[0]; - - statusMessage.should.have.a.property("error"); - statusMessage.error.should.have.a.property("message","my-error"); - statusMessage.error.should.have.a.property("source"); - statusMessage.error.source.should.have.a.property("id",sfInstanceId); - statusMessage.error.source.should.have.a.property("type","test2"); - statusMessage.error.source.should.have.a.property("name",undefined); - - currentNodes[catchInstanceId2].should.have.a.property("handled",0); - - currentNodes[catchInstanceId3].should.have.a.property("handled",1); - statusMessage = currentNodes[catchInstanceId3].messages[0]; - - statusMessage.should.have.a.property("error"); - statusMessage.error.should.have.a.property("message","my-error"); - statusMessage.error.should.have.a.property("source"); - statusMessage.error.source.should.have.a.property("id",sfInstanceId); - statusMessage.error.source.should.have.a.property("type","test2"); - statusMessage.error.source.should.have.a.property("name",undefined); - - flow.stop().then(function() { - - done(); - }); - }); - it("passes an error event to the subflow's parent tab catch node",function(done) { - var config = flowUtils.parseConfig([ - {id:"t1",type:"tab"}, - {id:"1",x:10,y:10,z:"t1",type:"test",name:"a",wires:["2"]}, - {id:"2",x:10,y:10,z:"t1",type:"subflow:sf1",wires:["3"]}, - {id:"3",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]}, - {id:"sf1",type:"subflow","name":"Subflow 2","info":"", - "in":[{"wires":[{"id":"sf1-1"}]}],"out":[{"wires":[{"id":"sf1-1","port":0}]}]}, - {id:"sf1-1",type:"test2","z":"sf1",x:166,y:99,"wires":[[]]}, - {id:"sn",x:10,y:10,z:"t1",type:"catch",foo:"a",wires:[]} - ]); - var flow = Flow.create(config,config.flows["t1"]); - - getNode.restore(); - getNode = sinon.stub(flows,"get",function(id) { - return flow.getNode(id); - }); - - flow.start(); - - var activeNodes = flow.getActiveNodes(); - var sfInstanceId = Object.keys(activeNodes)[3]; - - flow.handleError(activeNodes[sfInstanceId],"my-error",{a:"foo"}); - - currentNodes["sn"].should.have.a.property("handled",1); - var statusMessage = currentNodes["sn"].messages[0]; - - statusMessage.should.have.a.property("error"); - statusMessage.error.should.have.a.property("message","my-error"); - statusMessage.error.should.have.a.property("source"); - statusMessage.error.source.should.have.a.property("id",sfInstanceId); - statusMessage.error.source.should.have.a.property("type","test2"); - statusMessage.error.source.should.have.a.property("name",undefined); - - flow.stop().then(function() { - done(); - }); - }); it("moves any existing error object sideways",function(done){ var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, @@ -1075,12 +587,7 @@ describe('Flow', function() { {id:"3",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]}, {id:"sn",x:10,y:10,z:"t1",type:"catch",foo:"a",wires:[]} ]); - var flow = Flow.create(config,config.flows["t1"]); - - getNode.restore(); - getNode = sinon.stub(flows,"get",function(id) { - return flow.getNode(id); - }); + var flow = Flow.create({},config,config.flows["t1"]); flow.start(); diff --git a/test/unit/@node-red/runtime/lib/nodes/flows/Subflow_spec.js b/test/unit/@node-red/runtime/lib/nodes/flows/Subflow_spec.js new file mode 100644 index 000000000..4b9d8a928 --- /dev/null +++ b/test/unit/@node-red/runtime/lib/nodes/flows/Subflow_spec.js @@ -0,0 +1,460 @@ +/** + * Copyright JS Foundation and other contributors, http://js.foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + + +var should = require("should"); +var sinon = require('sinon'); +var clone = require('clone'); +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 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 Node = NR_TEST_UTILS.require("@node-red/runtime/lib/nodes/Node"); +var typeRegistry = NR_TEST_UTILS.require("@node-red/registry"); + +describe('Subflow', function() { + var getType; + + var stoppedNodes = {}; + var currentNodes = {}; + var rewiredNodes = {}; + var createCount = 0; + + beforeEach(function() { + currentNodes = {}; + stoppedNodes = {}; + rewiredNodes = {}; + createCount = 0; + var runtime = { + settings:{}, + log:{ + log: sinon.stub(), // function() { console.log("l",[...arguments].map(a => JSON.stringify(a)).join(" ")) },// + debug: sinon.stub(), // function() { console.log("d",[...arguments].map(a => JSON.stringify(a)).join(" ")) },//sinon.stub(), + trace: sinon.stub(), // function() { console.log("t",[...arguments].map(a => JSON.stringify(a)).join(" ")) },//sinon.stub(), + warn: sinon.stub(), // function() { console.log("w",[...arguments].map(a => JSON.stringify(a)).join(" ")) },//sinon.stub(), + info: sinon.stub(), // function() { console.log("i",[...arguments].map(a => JSON.stringify(a)).join(" ")) },//sinon.stub(), + metric: sinon.stub(), + _: function() { return "abc"} + } + } + Flow.init(runtime); + Subflow.init(runtime); + }); + + var TestNode = function(n) { + Node.call(this,n); + this._index = createCount++; + this.scope = n.scope; + var node = this; + this.foo = n.foo; + this.handled = 0; + this.stopped = false; + currentNodes[node.id] = node; + this.on('input',function(msg) { + // console.log(this.id,msg.payload); + node.handled++; + node.send(msg); + }); + this.on('close',function() { + node.stopped = true; + stoppedNodes[node.id] = node; + delete currentNodes[node.id]; + }); + this.__updateWires = this.updateWires; + this.updateWires = function(newWires) { + rewiredNodes[node.id] = node; + node.newWires = newWires; + node.__updateWires(newWires); + }; + } + util.inherits(TestNode,Node); + + var TestErrorNode = function(n) { + Node.call(this,n); + this._index = createCount++; + this.scope = n.scope; + this.name = n.name; + var node = this; + this.foo = n.foo; + this.handled = 0; + this.stopped = false; + currentNodes[node.id] = node; + this.on('input',function(msg) { + node.handled++; + node.error("test error",msg); + }); + this.on('close',function() { + node.stopped = true; + stoppedNodes[node.id] = node; + delete currentNodes[node.id]; + }); + this.__updateWires = this.updateWires; + this.updateWires = function(newWires) { + rewiredNodes[node.id] = node; + node.newWires = newWires; + node.__updateWires(newWires); + }; + } + util.inherits(TestErrorNode,Node); + + + var TestStatusNode = function(n) { + Node.call(this,n); + this._index = createCount++; + this.scope = n.scope; + this.name = n.name; + var node = this; + this.foo = n.foo; + this.handled = 0; + this.stopped = false; + currentNodes[node.id] = node; + this.on('input',function(msg) { + node.handled++; + node.status({text:"test status"}); + }); + this.on('close',function() { + node.stopped = true; + stoppedNodes[node.id] = node; + delete currentNodes[node.id]; + }); + this.__updateWires = this.updateWires; + this.updateWires = function(newWires) { + rewiredNodes[node.id] = node; + node.newWires = newWires; + node.__updateWires(newWires); + }; + } + util.inherits(TestStatusNode,Node); + + var TestAsyncNode = function(n) { + Node.call(this,n); + var node = this; + this.scope = n.scope; + this.foo = n.foo; + this.handled = 0; + this.messages = []; + this.stopped = false; + this.closeDelay = n.closeDelay || 50; + currentNodes[node.id] = node; + this.on('input',function(msg) { + node.handled++; + node.messages.push(msg); + node.send(msg); + }); + this.on('close',function(done) { + setTimeout(function() { + node.stopped = true; + stoppedNodes[node.id] = node; + delete currentNodes[node.id]; + done(); + },node.closeDelay); + }); + } + util.inherits(TestAsyncNode,Node); + + before(function() { + getType = sinon.stub(typeRegistry,"get",function(type) { + if (type=="test") { + return TestNode; + } else if (type=="testError"){ + return TestErrorNode; + } else if (type=="testStatus"){ + return TestStatusNode; + } else { + return TestAsyncNode; + } + }); + }); + after(function() { + getType.restore(); + }); + describe('#start',function() { + it("instantiates a subflow and stops it",function(done) { + 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:"subflow:sf1",wires:["3","4"]}, + {id:"3",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]}, + {id:"4",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]}, + {id:"sf1",type:"subflow","name":"Subflow 2","info":"", + "in":[{"wires":[{"id":"sf1-1"}]}],"out":[{"wires":[{"id":"sf1-2","port":0}]},{"wires":[{"id":"sf1","port":0}]}]}, + {id:"sf1-1",type:"test","z":"sf1",x:166,y:99,"wires":[["sf1-2"]]}, + {id:"sf1-2",type:"test","z":"sf1",foo:"sf1-cn",x:166,y:99,"wires":[[]]}, + {id:"sf1-cn",type:"test","z":"sf1"} + ]); + var flow = Flow.create({handleError: (a,b,c) => { console.log(a,b,c); }},config,config.flows["t1"]); + + flow.start(); + + var activeNodes = flow.getActiveNodes(); + Object.keys(activeNodes).should.have.length(4); + // var sfInstanceId = Object.keys(activeNodes)[5]; + // var sfInstanceId2 = Object.keys(activeNodes)[6]; + var sfConfigId = Object.keys(activeNodes)[4]; + + flow.getNode('1').should.have.a.property('id','1'); + flow.getNode('2').should.have.a.property('id','2'); + flow.getNode('3').should.have.a.property('id','3'); + flow.getNode('4').should.have.a.property('id','4'); + // flow.getNode(sfInstanceId).should.have.a.property('id',sfInstanceId); + // flow.getNode(sfInstanceId2).should.have.a.property('id',sfInstanceId2); + // flow.getNode(sfConfigId).should.have.a.property('id',sfConfigId); + + // flow.getNode(sfInstanceId2).should.have.a.property('foo',sfConfigId); + + Object.keys(currentNodes).should.have.length(6); + + currentNodes.should.have.a.property("1"); + currentNodes.should.not.have.a.property("2"); + currentNodes.should.have.a.property("3"); + currentNodes.should.have.a.property("4"); + // currentNodes.should.have.a.property(sfInstanceId); + // currentNodes.should.have.a.property(sfInstanceId2); + // currentNodes.should.have.a.property(sfConfigId); + + currentNodes["1"].should.have.a.property("handled",0); + currentNodes["3"].should.have.a.property("handled",0); + currentNodes["4"].should.have.a.property("handled",0); + // currentNodes[sfInstanceId].should.have.a.property("handled",0); + // currentNodes[sfInstanceId2].should.have.a.property("handled",0); + + currentNodes["1"].receive({payload:"test"}); + + currentNodes["1"].should.have.a.property("handled",1); + // currentNodes[sfInstanceId].should.have.a.property("handled",1); + // currentNodes[sfInstanceId2].should.have.a.property("handled",1); + currentNodes["3"].should.have.a.property("handled",1); + currentNodes["4"].should.have.a.property("handled",1); + + + + flow.stop().then(function() { + Object.keys(currentNodes).should.have.length(0); + Object.keys(stoppedNodes).should.have.length(6); + + // currentNodes.should.not.have.a.property("1"); + // currentNodes.should.not.have.a.property("3"); + // currentNodes.should.not.have.a.property("4"); + // // currentNodes.should.not.have.a.property(sfInstanceId); + // // currentNodes.should.not.have.a.property(sfInstanceId2); + // // currentNodes.should.not.have.a.property(sfConfigId); + // stoppedNodes.should.have.a.property("1"); + // stoppedNodes.should.have.a.property("3"); + // stoppedNodes.should.have.a.property("4"); + // // stoppedNodes.should.have.a.property(sfInstanceId); + // // stoppedNodes.should.have.a.property(sfInstanceId2); + // // stoppedNodes.should.have.a.property(sfConfigId); + done(); + }); + }); + + it("instantiates a subflow inside a subflow and stops it",function(done) { + 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:"subflow:sf1",wires:["3","4"]}, + {id:"3",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]}, + {id:"4",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]}, + {id:"sf1",type:"subflow","name":"Subflow 1","info":"", + "in":[{"wires":[{"id":"sf1-1"}]}],"out":[{"wires":[{"id":"sf1-2","port":0}]}]}, + {id:"sf2",type:"subflow","name":"Subflow 2","info":"", + "in":[{wires:[]}],"out":[{"wires":[{"id":"sf2","port":0}]}]}, + {id:"sf1-1",type:"test","z":"sf1",x:166,y:99,"wires":[["sf1-2"]]}, + {id:"sf1-2",type:"subflow:sf2","z":"sf1",x:166,y:99,"wires":[[]]} + + ]); + var flow = Flow.create({},config,config.flows["t1"]); + + flow.start(); + + currentNodes["1"].should.have.a.property("handled",0); + currentNodes["3"].should.have.a.property("handled",0); + + currentNodes["1"].receive({payload:"test"}); + + currentNodes["1"].should.have.a.property("handled",1); + + + currentNodes["3"].should.have.a.property("handled",1); + + + + flow.stop().then(function() { + Object.keys(currentNodes).should.have.length(0); + done(); + }); + }); + it("rewires a subflow node on update/start",function(done){ + + var rawConfig = [ + {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:"subflow:sf1",wires:["3"]}, + {id:"3",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]}, + {id:"4",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]}, + {id:"sf1",type:"subflow","name":"Subflow 2","info":"", + "in":[{"wires":[{"id":"sf1-1"}]}],"out":[{"wires":[{"id":"sf1-2","port":0}]}]}, + {id:"sf1-1",type:"test1","z":"sf1",x:166,y:99,"wires":[["sf1-2"]]}, + {id:"sf1-2",type:"test2","z":"sf1",x:166,y:99,"wires":[[]]} + ]; + + var config = flowUtils.parseConfig(clone(rawConfig)); + + rawConfig[2].wires = [["4"]]; + + var newConfig = flowUtils.parseConfig(rawConfig); + var diff = flowUtils.diffConfigs(config,newConfig); + var flow = Flow.create({},config,config.flows["t1"]); + + flow.start(); + + var activeNodes = flow.getActiveNodes(); + Object.keys(activeNodes).should.have.length(4); + // var sfInstanceId = Object.keys(activeNodes)[4]; + // var sfInstanceId2 = Object.keys(activeNodes)[5]; + + currentNodes["1"].should.have.a.property("handled",0); + currentNodes["3"].should.have.a.property("handled",0); + currentNodes["4"].should.have.a.property("handled",0); + + currentNodes["1"].receive({payload:"test"}); + + currentNodes["1"].should.have.a.property("handled",1); + // currentNodes[sfInstanceId].should.have.a.property("handled",1); + // currentNodes[sfInstanceId2].should.have.a.property("handled",1); + currentNodes["3"].should.have.a.property("handled",1); + currentNodes["4"].should.have.a.property("handled",0); + + flow.update(newConfig,newConfig.flows["t1"]); + flow.start(diff) + + currentNodes["1"].receive({payload:"test2"}); + + currentNodes["1"].should.have.a.property("handled",2); + // currentNodes[sfInstanceId].should.have.a.property("handled",2); + // currentNodes[sfInstanceId2].should.have.a.property("handled",2); + currentNodes["3"].should.have.a.property("handled",1); + currentNodes["4"].should.have.a.property("handled",1); + + + flow.stop().then(function() { + done(); + }); + }); + }); + describe('#stop', function() { + it("stops subflow instance nodes",function(done) { + 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:"subflow:sf1",wires:["3"]}, + {id:"3",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]}, + {id:"sf1",type:"subflow","name":"Subflow 2","info":"", + "in":[{"wires":[{"id":"sf1-1"}]}],"out":[{"wires":[{"id":"sf1-1","port":0}]}]}, + {id:"sf1-1",type:"test","z":"sf1",x:166,y:99,"wires":[[]]} + ]); + var flow = Flow.create({},config,config.flows["t1"]); + + flow.start(); + + var activeNodes = flow.getActiveNodes(); + Object.keys(activeNodes).should.have.length(3); + Object.keys(stoppedNodes).should.have.length(0); + flow.stop(["2"]).then(function() { + Object.keys(currentNodes).should.have.length(2); + Object.keys(stoppedNodes).should.have.length(1); + done(); + }).catch(done); + }); + }); + describe("#handleStatus",function() { + it("passes a status event to the subflow's parent tab status node",function(done) { + var config = flowUtils.parseConfig([ + {id:"t1",type:"tab"}, + {id:"1",x:10,y:10,z:"t1",type:"test",name:"a",wires:["2"]}, + {id:"2",x:10,y:10,z:"t1",type:"subflow:sf1",wires:["3"]}, + {id:"3",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]}, + {id:"sf1",type:"subflow","name":"Subflow 2","info":"", + "in":[{"wires":[{"id":"sf1-1"}]}],"out":[{"wires":[{"id":"sf1-1","port":0}]}]}, + {id:"sf1-1",type:"testStatus",name:"test-status-node","z":"sf1",x:166,y:99,"wires":[[]]}, + {id:"sn",x:10,y:10,z:"t1",type:"status",foo:"a",wires:[]} + ]); + var flow = Flow.create({},config,config.flows["t1"]); + + flow.start(); + + var activeNodes = flow.getActiveNodes(); + + activeNodes["1"].receive({payload:"test"}); + + currentNodes["sn"].should.have.a.property("handled",1); + var statusMessage = currentNodes["sn"].messages[0]; + + statusMessage.should.have.a.property("status"); + statusMessage.status.should.have.a.property("text","test status"); + statusMessage.status.should.have.a.property("source"); + statusMessage.status.source.should.have.a.property("type","testStatus"); + statusMessage.status.source.should.have.a.property("name","test-status-node"); + + flow.stop().then(function() { + + done(); + }); + }); + }); + + describe("#handleError",function() { + it("passes an error event to the subflow's parent tab catch node",function(done) { + var config = flowUtils.parseConfig([ + {id:"t1",type:"tab"}, + {id:"1",x:10,y:10,z:"t1",type:"test",name:"a",wires:["2"]}, + {id:"2",x:10,y:10,z:"t1",type:"subflow:sf1",wires:["3"]}, + {id:"3",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]}, + {id:"sf1",type:"subflow","name":"Subflow 2","info":"", + "in":[{"wires":[{"id":"sf1-1"}]}],"out":[{"wires":[{"id":"sf1-1","port":0}]}]}, + {id:"sf1-1",name:"test-error-node",type:"testError","z":"sf1",x:166,y:99,"wires":[[]]}, + {id:"sn",x:10,y:10,z:"t1",type:"catch",foo:"a",wires:[]} + ]); + var flow = Flow.create({},config,config.flows["t1"]); + + flow.start(); + + var activeNodes = flow.getActiveNodes(); + + activeNodes["1"].receive({payload:"test"}); + + currentNodes["sn"].should.have.a.property("handled",1); + var statusMessage = currentNodes["sn"].messages[0]; + + statusMessage.should.have.a.property("error"); + statusMessage.error.should.have.a.property("message","test error"); + statusMessage.error.should.have.a.property("source"); + statusMessage.error.source.should.have.a.property("type","testError"); + statusMessage.error.source.should.have.a.property("name","test-error-node"); + + flow.stop().then(function() { + done(); + }); + + }); + }); + +}); diff --git a/test/unit/@node-red/runtime/lib/nodes/flows/index_spec.js b/test/unit/@node-red/runtime/lib/nodes/flows/index_spec.js index cbaca5078..a37d88f78 100644 --- a/test/unit/@node-red/runtime/lib/nodes/flows/index_spec.js +++ b/test/unit/@node-red/runtime/lib/nodes/flows/index_spec.js @@ -70,7 +70,7 @@ describe('flows/index', function() { credentialsLoad = sinon.stub(credentials,"load",function() { return when.resolve(); }); - flowCreate = sinon.stub(Flow,"create",function(global, flow) { + flowCreate = sinon.stub(Flow,"create",function(parent, global, flow) { var id; if (typeof flow === 'undefined') { flow = global; @@ -354,113 +354,113 @@ describe('flows/index', function() { describe('#stopFlows', function() { }); - describe('#handleError', function() { - it('passes error to correct flow', function(done) { - var originalConfig = [ - {id:"t1-1",x:10,y:10,z:"t1",type:"test",wires:[]}, - {id:"t1",type:"tab"} - ]; - storage.getFlows = function() { - return when.resolve({flows:originalConfig}); - } - - events.once('nodes-started',function() { - flows.handleError(originalConfig[0],"message",{}); - flowCreate.flows['t1'].handleError.called.should.be.true(); - done(); - }); - - flows.init({log:mockLog, settings:{},storage:storage}); - flows.load().then(function() { - flows.startFlows(); - }); - }); - it('passes error to flows that use the originating global config', function(done) { - var originalConfig = [ - {id:"configNode",type:"test"}, - {id:"t1",type:"tab"}, - {id:"t1-1",x:10,y:10,z:"t1",type:"test",config:"configNode",wires:[]}, - {id:"t2",type:"tab"}, - {id:"t2-1",x:10,y:10,z:"t2",type:"test",wires:[]}, - {id:"t3",type:"tab"}, - {id:"t3-1",x:10,y:10,z:"t3",type:"test",config:"configNode",wires:[]} - ]; - storage.getFlows = function() { - return when.resolve({flows:originalConfig}); - } - - events.once('nodes-started',function() { - flows.handleError(originalConfig[0],"message",{}); - try { - flowCreate.flows['t1'].handleError.called.should.be.true(); - flowCreate.flows['t2'].handleError.called.should.be.false(); - flowCreate.flows['t3'].handleError.called.should.be.true(); - done(); - } catch(err) { - done(err); - } - }); - - flows.init({log:mockLog, settings:{},storage:storage}); - flows.load().then(function() { - flows.startFlows(); - }); - }); - }); - describe('#handleStatus', function() { - it('passes status to correct flow', function(done) { - var originalConfig = [ - {id:"t1-1",x:10,y:10,z:"t1",type:"test",wires:[]}, - {id:"t1",type:"tab"} - ]; - storage.getFlows = function() { - return when.resolve({flows:originalConfig}); - } - - events.once('nodes-started',function() { - flows.handleStatus(originalConfig[0],"message"); - flowCreate.flows['t1'].handleStatus.called.should.be.true(); - done(); - }); - - flows.init({log:mockLog, settings:{},storage:storage}); - flows.load().then(function() { - flows.startFlows(); - }); - }); - - it('passes status to flows that use the originating global config', function(done) { - var originalConfig = [ - {id:"configNode",type:"test"}, - {id:"t1",type:"tab"}, - {id:"t1-1",x:10,y:10,z:"t1",type:"test",config:"configNode",wires:[]}, - {id:"t2",type:"tab"}, - {id:"t2-1",x:10,y:10,z:"t2",type:"test",wires:[]}, - {id:"t3",type:"tab"}, - {id:"t3-1",x:10,y:10,z:"t3",type:"test",config:"configNode",wires:[]} - ]; - storage.getFlows = function() { - return when.resolve({flows:originalConfig}); - } - - events.once('nodes-started',function() { - flows.handleStatus(originalConfig[0],"message"); - try { - flowCreate.flows['t1'].handleStatus.called.should.be.true(); - flowCreate.flows['t2'].handleStatus.called.should.be.false(); - flowCreate.flows['t3'].handleStatus.called.should.be.true(); - done(); - } catch(err) { - done(err); - } - }); - - flows.init({log:mockLog, settings:{},storage:storage}); - flows.load().then(function() { - flows.startFlows(); - }); - }); - }); + // describe('#handleError', function() { + // it('passes error to correct flow', function(done) { + // var originalConfig = [ + // {id:"t1-1",x:10,y:10,z:"t1",type:"test",wires:[]}, + // {id:"t1",type:"tab"} + // ]; + // storage.getFlows = function() { + // return when.resolve({flows:originalConfig}); + // } + // + // events.once('nodes-started',function() { + // flows.handleError(originalConfig[0],"message",{}); + // flowCreate.flows['t1'].handleError.called.should.be.true(); + // done(); + // }); + // + // flows.init({log:mockLog, settings:{},storage:storage}); + // flows.load().then(function() { + // flows.startFlows(); + // }); + // }); + // it('passes error to flows that use the originating global config', function(done) { + // var originalConfig = [ + // {id:"configNode",type:"test"}, + // {id:"t1",type:"tab"}, + // {id:"t1-1",x:10,y:10,z:"t1",type:"test",config:"configNode",wires:[]}, + // {id:"t2",type:"tab"}, + // {id:"t2-1",x:10,y:10,z:"t2",type:"test",wires:[]}, + // {id:"t3",type:"tab"}, + // {id:"t3-1",x:10,y:10,z:"t3",type:"test",config:"configNode",wires:[]} + // ]; + // storage.getFlows = function() { + // return when.resolve({flows:originalConfig}); + // } + // + // events.once('nodes-started',function() { + // flows.handleError(originalConfig[0],"message",{}); + // try { + // flowCreate.flows['t1'].handleError.called.should.be.true(); + // flowCreate.flows['t2'].handleError.called.should.be.false(); + // flowCreate.flows['t3'].handleError.called.should.be.true(); + // done(); + // } catch(err) { + // done(err); + // } + // }); + // + // flows.init({log:mockLog, settings:{},storage:storage}); + // flows.load().then(function() { + // flows.startFlows(); + // }); + // }); + // }); + // describe('#handleStatus', function() { + // it('passes status to correct flow', function(done) { + // var originalConfig = [ + // {id:"t1-1",x:10,y:10,z:"t1",type:"test",wires:[]}, + // {id:"t1",type:"tab"} + // ]; + // storage.getFlows = function() { + // return when.resolve({flows:originalConfig}); + // } + // + // events.once('nodes-started',function() { + // flows.handleStatus(originalConfig[0],"message"); + // flowCreate.flows['t1'].handleStatus.called.should.be.true(); + // done(); + // }); + // + // flows.init({log:mockLog, settings:{},storage:storage}); + // flows.load().then(function() { + // flows.startFlows(); + // }); + // }); + // + // it('passes status to flows that use the originating global config', function(done) { + // var originalConfig = [ + // {id:"configNode",type:"test"}, + // {id:"t1",type:"tab"}, + // {id:"t1-1",x:10,y:10,z:"t1",type:"test",config:"configNode",wires:[]}, + // {id:"t2",type:"tab"}, + // {id:"t2-1",x:10,y:10,z:"t2",type:"test",wires:[]}, + // {id:"t3",type:"tab"}, + // {id:"t3-1",x:10,y:10,z:"t3",type:"test",config:"configNode",wires:[]} + // ]; + // storage.getFlows = function() { + // return when.resolve({flows:originalConfig}); + // } + // + // events.once('nodes-started',function() { + // flows.handleStatus(originalConfig[0],"message"); + // try { + // flowCreate.flows['t1'].handleStatus.called.should.be.true(); + // flowCreate.flows['t2'].handleStatus.called.should.be.false(); + // flowCreate.flows['t3'].handleStatus.called.should.be.true(); + // done(); + // } catch(err) { + // done(err); + // } + // }); + // + // flows.init({log:mockLog, settings:{},storage:storage}); + // flows.load().then(function() { + // flows.startFlows(); + // }); + // }); + // }); describe('#checkTypeInUse', function() {