From ab87fa9ce428b27c11803cf71616e68021205b09 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Mon, 2 Nov 2015 20:41:59 +0000 Subject: [PATCH] Ensure status/errors from global config nodes propagate properly --- red/nodes/Node.js | 3 + red/nodes/flows/Flow.js | 1 - red/nodes/flows/index.js | 48 +++++- red/nodes/flows/util.js | 11 ++ test/red/nodes/flows/Flow_spec.js | 12 +- test/red/nodes/flows/index_spec.js | 241 ++++++++--------------------- test/red/nodes/flows/util_spec.js | 10 ++ 7 files changed, 141 insertions(+), 185 deletions(-) diff --git a/red/nodes/Node.js b/red/nodes/Node.js index ee988ed1f..1bebaa0e1 100644 --- a/red/nodes/Node.js +++ b/red/nodes/Node.js @@ -33,6 +33,9 @@ function Node(n) { if (n.name) { this.name = n.name; } + if (n._alias) { + this._alias = n._alias; + } this.updateWires(n.wires); } diff --git a/red/nodes/flows/Flow.js b/red/nodes/flows/Flow.js index 562f1a9be..cc35355f3 100644 --- a/red/nodes/flows/Flow.js +++ b/red/nodes/flows/Flow.js @@ -187,7 +187,6 @@ function Flow(global,flow) { } } } - var targetCatchNodes = null; var throwingNode = node; var handled = false; diff --git a/red/nodes/flows/index.js b/red/nodes/flows/index.js index f8d7e62c5..2a1149df6 100644 --- a/red/nodes/flows/index.js +++ b/red/nodes/flows/index.js @@ -38,6 +38,7 @@ var activeFlows = {}; var started = false; var activeNodesToFlow = {}; +var subflowInstanceNodeMap = {}; var typeEventRegistered = false; @@ -151,19 +152,47 @@ function getConfig() { return activeConfig; } -function handleError(node,logMessage,msg) { +function delegateError(node,logMessage,msg) { if (activeFlows[node.z]) { activeFlows[node.z].handleError(node,logMessage,msg); } else if (activeNodesToFlow[node.z]) { activeFlows[activeNodesToFlow[node.z]].handleError(node,logMessage,msg); + } else if (activeFlowConfig.subflows[node.z]) { + subflowInstanceNodeMap[node.id].forEach(function(n) { + delegateError(getNode(n),logMessage,msg); + }); + } +} +function handleError(node,logMessage,msg) { + if (node.z) { + delegateError(node,logMessage,msg); + } else { + if (activeFlowConfig.configs[node.id]) { + activeFlowConfig.configs[node.id]._users.forEach(function(id) { + var userNode = activeFlowConfig.allNodes[id]; + delegateError(userNode,logMessage,msg); + }) + } } } -function handleStatus(node,statusMessage) { +function delegateStatus(node,statusMessage) { if (activeFlows[node.z]) { activeFlows[node.z].handleStatus(node,statusMessage); } } +function handleStatus(node,statusMessage) { + if (node.z) { + delegateStatus(node,statusMessage); + } else { + if (activeFlowConfig.configs[node.id]) { + activeFlowConfig.configs[node.id]._users.forEach(function(id) { + var userNode = activeFlowConfig.allNodes[id]; + delegateStatus(userNode,statusMessage); + }) + } + } +} function start(type,diff) { @@ -223,7 +252,12 @@ function start(type,diff) { var activeNodes = activeFlows[id].getActiveNodes(); Object.keys(activeNodes).forEach(function(nid) { activeNodesToFlow[nid] = id; + if (activeNodes[nid]._alias) { + subflowInstanceNodeMap[activeNodes[nid]._alias] = subflowInstanceNodeMap[activeNodes[nid]._alias] || []; + subflowInstanceNodeMap[activeNodes[nid]._alias].push(nid); + } }); + } } events.emit("nodes-started"); @@ -267,6 +301,16 @@ function stop(type,diff) { } } } + if (stopList) { + stopList.forEach(function(id) { + delete activeNodesToFlow[id]; + }); + } + // Ideally we'd prune just what got stopped - but mapping stopList + // id to the list of subflow instance nodes is something only Flow + // can do... so cheat by wiping the map knowing it'll be rebuilt + // in start() + subflowInstanceNodeMap = {}; if (diff) { log.info(log._("nodes.flows.stopped-modified-"+type)); } else { diff --git a/red/nodes/flows/util.js b/red/nodes/flows/util.js index 3c561772c..736078b86 100644 --- a/red/nodes/flows/util.js +++ b/red/nodes/flows/util.js @@ -97,11 +97,22 @@ module.exports = { container.configs[n.id] = n; } else { flow.configs[n.id] = n; + flow.configs[n.id]._users = []; } } } } }); + config.forEach(function(n) { + if (n.type !== 'subflow' && n.type !== 'tab') { + for (var prop in n) { + if (n.hasOwnProperty(prop) && prop !== 'id' && prop !== 'wires' && prop !== '_users' && flow.configs[n[prop]]) { + // This property references a global config node + flow.configs[n[prop]]._users.push(n.id) + } + } + } + }); return flow; }, diff --git a/test/red/nodes/flows/Flow_spec.js b/test/red/nodes/flows/Flow_spec.js index 21a007011..727351930 100644 --- a/test/red/nodes/flows/Flow_spec.js +++ b/test/red/nodes/flows/Flow_spec.js @@ -634,6 +634,7 @@ describe('Flow', function() { {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":[[]]}, @@ -651,10 +652,11 @@ describe('Flow', function() { flow.start(); var activeNodes = flow.getActiveNodes(); - var sfInstanceId = Object.keys(activeNodes)[3]; - var statusInstanceId = Object.keys(activeNodes)[4]; - var statusInstanceId2 = Object.keys(activeNodes)[5]; - var statusInstanceId3 = Object.keys(activeNodes)[6]; + + 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"}); @@ -668,6 +670,8 @@ describe('Flow', function() { 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); diff --git a/test/red/nodes/flows/index_spec.js b/test/red/nodes/flows/index_spec.js index 1983e1986..d8af1bc09 100644 --- a/test/red/nodes/flows/index_spec.js +++ b/test/red/nodes/flows/index_spec.js @@ -360,6 +360,37 @@ describe('flows/index', function() { done(); }); + flows.init({},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(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({},storage); flows.load().then(function() { flows.startFlows(); @@ -387,183 +418,37 @@ describe('flows/index', 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(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({},storage); + flows.load().then(function() { + flows.startFlows(); + }); + }); }); }); - - // afterEach(function(done) { - // flows.stopFlows().then(function() { - // loadFlows([],done); - // }); - // }); - - // describe('#load',function() { - // - // it('should load nothing when storage is empty',function(done) { - // loadFlows([], done); - // }); - // - // it.skip('should load and start an empty tab flow',function(done) { - // events.once('nodes-started', function() { done(); }); - // loadFlows([{"type":"tab","id":"tab1","label":"Sheet 1"}], function() {}); - // }); - // - // it.skip('should load and start a registered node type', function(done) { - // RED.registerType('debug', function() {}); - // var typeRegistryGet = sinon.stub(typeRegistry,"get",function(nt) { - // return RedNode; - // }); - // loadFlows([{"id":"n1","type":"debug"}], function() { }); - // events.once('nodes-started', function() { - // typeRegistryGet.restore(); - // done(); - // }); - // }); - // - // it.skip('should load and start when node type is registered', function(done) { - // var typeRegistryGet = sinon.stub(typeRegistry,"get"); - // typeRegistryGet.onCall(0).returns(null); - // typeRegistryGet.returns(RedNode); - // loadFlows([{"id":"n2","type":"inject"}], function() { - // events.emit('type-registered','inject'); - // }); - // events.once('nodes-started', function() { - // typeRegistryGet.restore(); - // done(); - // }); - // }); - // - // it.skip('should not instantiate nodes of an unused subflow', function(done) { - // RED.registerType('abc', function() {}); - // var typeRegistryGet = sinon.stub(typeRegistry,"get",function(nt) { - // return RedNode; - // }); - // loadFlows([{"id":"n1","type":"subflow",inputs:[],outputs:[],wires:[]}, - // {"id":"n2","type":"abc","z":"n1",wires:[]} - // ],function() { }); - // events.once('nodes-started', function() { - // (flows.get("n2") == null).should.be.true; - // var ncount = 0 - // flows.eachNode(function(n) { - // ncount++; - // }); - // ncount.should.equal(0); - // typeRegistryGet.restore(); - // done(); - // }); - // }); - // it.skip('should instantiate nodes of an used subflow with new IDs', function(done) { - // RED.registerType('abc', function() {}); - // var typeRegistryGet = sinon.stub(typeRegistry,"get",function(nt) { - // return RedNode; - // }); - // loadFlows([{"id":"n1","type":"subflow",inputs:[],outputs:[]}, - // {"id":"n2","type":"abc","z":"n1","name":"def",wires:[]}, - // {"id":"n3","type":"subflow:n1"} - // ], function() { }); - // events.once('nodes-started', function() { - // // n2 should not get instantiated with that id - // (flows.get("n2") == null).should.be.true; - // var ncount = 0 - // var nodes = []; - // flows.eachNode(function(n) { - // nodes.push(n); - // }); - // nodes.should.have.lengthOf(2); - // - // // Assume the nodes are instantiated in this order - not - // // a requirement, but makes the test easier to write. - // nodes[0].should.have.property("id","n3"); - // nodes[0].should.have.property("type","subflow:n1"); - // nodes[1].should.not.have.property("id","n2"); - // nodes[1].should.have.property("name","def"); - // - // // TODO: verify instance wiring is correct - // typeRegistryGet.restore(); - // done(); - // }); - // }); - // }); - // - // describe.skip('#setFlows',function() { - // var credentialsExtact; - // var credentialsSave; - // var stopFlows; - // var startFlows; - // var credentialsExtractNode; - // beforeEach(function() { - // credentialsExtact = sinon.stub(credentials,"extract",function(node) {credentialsExtractNode = clone(node);delete node.credentials;}); - // credentialsSave = sinon.stub(credentials,"save",function() { return when.resolve();}); - // stopFlows = sinon.stub(flows,"stopFlows",function() {return when.resolve();}); - // startFlows = sinon.stub(flows,"startFlows",function() {}); - // }); - // afterEach(function() { - // credentialsExtact.restore(); - // credentialsSave.restore(); - // startFlows.restore(); - // stopFlows.restore(); - // }); - // - // it('should extract credentials from nodes', function(done) { - // var testFlow = [{"type":"testNode","credentials":{"a":1}},{"type":"testNode2"}]; - // var resultFlow = clone(testFlow); - // var storage = { saveFlows: sinon.spy() }; - // flows.init({},storage); - // flows.setFlows(testFlow,"full").then(function() { - // try { - // credentialsExtact.calledOnce.should.be.true; - // // credential property stripped - // testFlow.should.not.have.property("credentials"); - // credentialsExtractNode.should.eql(resultFlow[0]); - // credentialsExtractNode.should.not.equal(resultFlow[0]); - // - // credentialsSave.calledOnce.should.be.true; - // - // storage.saveFlows.calledOnce.should.be.true; - // storage.saveFlows.args[0][0].should.eql(testFlow); - // - // stopFlows.calledOnce.should.be.true; - // startFlows.calledOnce.should.be.true; - // - // done(); - // } catch(err) { - // done(err); - // } - // }); - // }); - // - // it('should apply diff on partial deployment', function(done) { - // var testFlow = [{"type":"testNode"},{"type":"testNode2"}]; - // var testFlow2 = [{"type":"testNode3"},{"type":"testNode4"}]; - // var storage = { saveFlows: sinon.spy() }; - // flows.init({},storage); - // - // flows.setFlows(testFlow,"full").then(function() { - // flows.setFlows(testFlow2,"nodes").then(function() { - // try { - // credentialsExtact.called.should.be.false; - // - // storage.saveFlows.calledTwice.should.be.true; - // storage.saveFlows.args[1][0].should.eql(testFlow2); - // - // stopFlows.calledTwice.should.be.true; - // startFlows.calledTwice.should.be.true; - // - // var configDiff = { - // type: 'nodes', - // stop: [], - // rewire: [], - // config: testFlow2 - // } - // stopFlows.args[1][0].should.eql(configDiff); - // startFlows.args[1][0].should.eql(configDiff); - // - // done(); - // } catch(err) { - // done(err); - // } - // }); - // }); - // }); - // - // - // }); diff --git a/test/red/nodes/flows/util_spec.js b/test/red/nodes/flows/util_spec.js index 47f08df6d..40341ce46 100644 --- a/test/red/nodes/flows/util_spec.js +++ b/test/red/nodes/flows/util_spec.js @@ -90,7 +90,17 @@ describe('flows/util', function() { var expectedConfig = {"allNodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"test","wires":[]},"t1":{"id":"t1","type":"tab"}},"subflows":{},"configs":{},"flows":{"t1":{"id":"t1","type":"tab","subflows":{},"configs":{},"nodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"test","wires":[]}}}},"missingTypes":[]}; redUtil.compareObjects(parsedConfig,expectedConfig).should.be.true; + }); + it('parses a single-tab flow with global config node', function() { + var originalConfig = [ + {id:"t1-1",x:10,y:10,z:"t1",type:"test",foo:"cn", wires:[]}, + {id:"cn",type:"test"}, + {id:"t1",type:"tab"} + ]; + var parsedConfig = flowUtil.parseConfig(originalConfig); + var expectedConfig = {"allNodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"test","foo":"cn","wires":[]},"cn":{"id":"cn","type":"test"},"t1":{"id":"t1","type":"tab"}},"subflows":{},"configs":{"cn":{"id":"cn","type":"test","_users":["t1-1"]}},"flows":{"t1":{"id":"t1","type":"tab","subflows":{},"configs":{},"nodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"test","foo":"cn","wires":[]}}}},"missingTypes":[]}; + redUtil.compareObjects(parsedConfig,expectedConfig).should.be.true; }); it('parses a multi-tab flow', function() {