From ec25191c98a02b63417bddfa99528cee3ab8d83e Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Sun, 11 Oct 2015 20:37:11 +0100 Subject: [PATCH 1/4] Flow Engine refactor Each flow/tab now exists as its own logical object. This is the ground work for allowing flows to be added/removed/updated independently. --- red/nodes/Flow.js | 845 ------------------ red/nodes/flows.js | 199 ----- red/nodes/flows/Flow.js | 467 ++++++++++ red/nodes/flows/index.js | 318 +++++++ red/nodes/flows/util.js | 316 +++++++ red/nodes/index.js | 2 +- red/nodes/{ => registry}/deprecated.js | 0 red/server.js | 2 +- test/red/nodes/{ => flows}/Flow_spec.js | 819 +++-------------- test/red/nodes/flows/index_spec.js | 415 +++++++++ test/red/nodes/flows/util_spec.js | 594 ++++++++++++ test/red/nodes/flows_spec.js | 230 ----- .../nodes/{ => registry}/deprecated_spec.js | 4 +- test/red/server_spec.js | 67 +- 14 files changed, 2275 insertions(+), 2003 deletions(-) delete mode 100644 red/nodes/Flow.js delete mode 100644 red/nodes/flows.js create mode 100644 red/nodes/flows/Flow.js create mode 100644 red/nodes/flows/index.js create mode 100644 red/nodes/flows/util.js rename red/nodes/{ => registry}/deprecated.js (100%) rename test/red/nodes/{ => flows}/Flow_spec.js (52%) create mode 100644 test/red/nodes/flows/index_spec.js create mode 100644 test/red/nodes/flows/util_spec.js delete mode 100644 test/red/nodes/flows_spec.js rename test/red/nodes/{ => registry}/deprecated_spec.js (92%) diff --git a/red/nodes/Flow.js b/red/nodes/Flow.js deleted file mode 100644 index bec251138..000000000 --- a/red/nodes/Flow.js +++ /dev/null @@ -1,845 +0,0 @@ -/** - * Copyright 2015 IBM Corp. - * - * 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 when = require("when"); -var clone = require("clone"); - -var typeRegistry = require("./registry"); -var credentials = require("./credentials"); -var redUtil = require("../util"); -var events = require("../events"); -var Log = require("../log"); - -function getID() { - return (1+Math.random()*4294967295).toString(16); -} - -var EnvVarPropertyRE = /^\$\((\S+)\)$/; - -function mapEnvVarProperties(obj,prop) { - if (Buffer.isBuffer(obj[prop])) { - return; - } else if (Array.isArray(obj[prop])) { - for (var i=0;i 0) { - throw new Error(Log._("nodes.flow.missing-types")); - } - events.emit("nodes-starting"); - - var id; - var node; - - for (id in this.configNodes) { - if (this.configNodes.hasOwnProperty(id)) { - node = this.configNodes[id]; - if (!this.activeNodes[id]) { - this.activeNodes[id] = createNode(node.type,node); - } - } - } - - for (id in this.nodes) { - if (this.nodes.hasOwnProperty(id)) { - node = this.nodes[id]; - if (!node.subflow) { - if (!this.activeNodes[id]) { - this.activeNodes[id] = createNode(node.type,node.config); - //console.log(id,"created"); - } else { - //console.log(id,"already running"); - } - } else { - if (!this.subflowInstanceNodes[id]) { - var nodes = createSubflow(this.subflows[node.subflow],node.config,this.subflows); - this.subflowInstanceNodes[id] = nodes.map(function(n) { return n.id}); - for (var i=0;i 0) { - var i = this.missingTypes.indexOf(type); - if (i != -1) { - this.missingTypes.splice(i,1); - if (this.missingTypes.length === 0 && this.started) { - this.start(); - } - return true; - } - } - return false; - -} - -Flow.prototype.getNode = function(id) { - return this.activeNodes[id]; -} - -Flow.prototype.getFlow = function() { - //console.log(this.config); - return this.config; -} - -Flow.prototype.eachNode = function(callback) { - for (var id in this.activeNodes) { - if (this.activeNodes.hasOwnProperty(id)) { - callback(this.activeNodes[id]); - } - } -} - -Flow.prototype.diffConfig = function(config,type) { - - var activeNodesToStop = []; - var nodesToRewire = []; - - if (type && type!="full") { - var diff = diffFlow(this,config); - //var diff = { - // deleted:[] - // changed:[] - // linked:[] - // wiringChanged: [] - //} - - var nodesToStop = []; - nodesToRewire = diff.wiringChanged; - - if (type == "nodes") { - nodesToStop = diff.deleted.concat(diff.changed); - } else if (type == "flows") { - nodesToStop = diff.deleted.concat(diff.changed).concat(diff.linked); - } - - for (var i=0;i 0) { - var subflowId = changedSubflowStack.pop(); - - config.forEach(function(node) { - if (node.type == "subflow:"+subflowId) { - if (!changedNodes[node.id]) { - changedNodes[node.id] = node; - checkSubflowMembership(flowNodes,node.id); - } - } - }); - - } - - config.forEach(function(node) { - buildNodeLinks(newLinks,node,flowNodes); - }); - - var markLinkedNodes = function(linkChanged,otherChangedNodes,linkMap,allNodes) { - var stack = Object.keys(changedNodes).concat(Object.keys(otherChangedNodes)); - var visited = {}; - - while(stack.length > 0) { - var id = stack.pop(); - var linkedNodes = linkMap[id]; - if (linkedNodes) { - for (var i=0;i 0) { - log.info(log._("nodes.flows.missing-types")); - var knownUnknowns = 0; - for (var i=0;i 0) { - log.info(log._("nodes.flows.missing-type-install-1")); - log.info(" npm install "); - log.info(log._("nodes.flows.missing-type-install-2")); - log.info(" "+settings.userDir); - } - } - } - }, - stopFlows: function(configDiff) { - if (configDiff) { - log.info(log._("nodes.flows.stopping-modified-"+configDiff.type)); - } else { - log.info(log._("nodes.flows.stopping-flows")); - } - if (activeFlow) { - return activeFlow.stop(configDiff).then(function() { - if (configDiff) { - log.info(log._("nodes.flows.stopped-modified-"+configDiff.type)); - } else { - log.info(log._("nodes.flows.stopped-flows")); - } - return; - }); - } else { - log.info(log._("nodes.flows.stopped")); - return; - } - }, - handleError: function(node,logMessage,msg) { - activeFlow.handleError(node,logMessage,msg); - }, - handleStatus: function(node,statusMessage) { - activeFlow.handleStatus(node,statusMessage); - } -}; - -var activeFlow = null; diff --git a/red/nodes/flows/Flow.js b/red/nodes/flows/Flow.js new file mode 100644 index 000000000..397537e19 --- /dev/null +++ b/red/nodes/flows/Flow.js @@ -0,0 +1,467 @@ +/** + * Copyright 2015 IBM Corp. + * + * 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 when = require("when"); +var clone = require("clone"); +var typeRegistry = require("../registry"); +var Log = require("../../log"); +var redUtil = require("../../util"); +var flowUtil = require("./util"); + +function Flow(global,flow) { + if (typeof flow === 'undefined') { + flow = global; + } + var activeNodes = {}; + var subflowInstanceNodes = {}; + var catchNodeMap = {}; + var statusNodeMap = {}; + + this.start = function(diff) { + var node; + var id; + catchNodeMap = {}; + statusNodeMap = {}; + for (id in flow.configs) { + if (flow.configs.hasOwnProperty(id)) { + node = flow.configs[id]; + if (!activeNodes[id]) { + activeNodes[id] = createNode(node.type,node); + } + } + } + if (diff) { + for (var j=0;j 0) { + var i = activeFlowConfig.missingTypes.indexOf(type); + if (i != -1) { + log.info(log._("nodes.flows.registered-missing", {type:type})); + activeFlowConfig.missingTypes.splice(i,1); + if (activeFlowConfig.missingTypes.length === 0 && started) { + start(); + } + } + } + }); +} +function load() { + return storage.getFlows().then(function(flows) { + return credentials.load().then(function() { + return setConfig(flows,"load"); + }); + }).otherwise(function(err) { + log.warn(log._("nodes.flows.error",{message:err.toString()})); + console.log(err.stack); + }); +} + +function setConfig(config,type) { + type = type||"full"; + + var credentialsChanged = false; + var credentialSavePromise = null; + var configSavePromise = null; + + var cleanConfig = clone(config); + cleanConfig.forEach(function(node) { + if (node.credentials) { + credentials.extract(node); + credentialsChanged = true; + } + }); + + if (credentialsChanged) { + credentialSavePromise = credentials.save(); + } else { + credentialSavePromise = when.resolve(); + } + if (type === 'load') { + configSavePromise = credentialSavePromise; + type = 'full'; + } else { + configSavePromise = credentialSavePromise.then(function() { + return storage.saveFlows(cleanConfig); + }); + } + + return configSavePromise + .then(function() { + var diff; + activeConfig = cleanConfig; + if (type === 'full') { + activeFlowConfig = flowUtil.parseConfig(clone(config)); + } else { + var newConfig = flowUtil.parseConfig(clone(config)); + diff = flowUtil.diffConfigs(activeFlowConfig,newConfig); + activeFlowConfig = newConfig; + } + credentials.clean(activeConfig).then(function() { + if (started) { + return stop(type,diff).then(function() { + start(type,diff); + }).otherwise(function(err) { + console.log(err); + }) + } + }); + }); +} + +function getNode(id) { + var node; + if (activeNodesToFlow[id]) { + return activeFlows[activeNodesToFlow[id]].getNode(id); + } + for (var flowId in activeFlows) { + if (activeFlows.hasOwnProperty(flowId)) { + node = activeFlows[flowId].getNode(id); + if (node) { + return node; + } + } + } + return null; +} + +function eachNode(cb) { + for (var id in activeFlowConfig.nodes) { + if (activeFlowConfig.nodes.hasOwnProperty(id)) { + cb(activeFlowConfig.nodes[id]); + } + } +} + +function getConfig() { + return activeConfig; +} + +function handleError(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); + } +} + +function handleStatus(node,statusMessage) { + if (activeFlows[node.z]) { + activeFlows[node.z].handleStatus(node,statusMessage); + } +} + + +function start(type,diff) { + type = type||"full"; + started = true; + var i; + if (activeFlowConfig.missingTypes.length > 0) { + log.info(log._("nodes.flows.missing-types")); + var knownUnknowns = 0; + for (i=0;i 0) { + log.info(log._("nodes.flows.missing-type-install-1")); + log.info(" npm install "); + log.info(log._("nodes.flows.missing-type-install-2")); + log.info(" "+settings.userDir); + } + return; + } + if (diff) { + log.info(log._("nodes.flows.starting-modified-"+type)); + } else { + log.info(log._("nodes.flows.starting-flows")); + } + var id; + if (!diff) { + activeFlows['_GLOBAL_'] = Flow.create(activeFlowConfig); + for (id in activeFlowConfig.flows) { + if (activeFlowConfig.flows.hasOwnProperty(id)) { + activeFlows[id] = Flow.create(activeFlowConfig,activeFlowConfig.flows[id]); + } + } + } else { + activeFlows['_GLOBAL_'].update(activeFlowConfig,activeFlowConfig); + for (id in activeFlowConfig.flows) { + if (activeFlowConfig.flows.hasOwnProperty(id)) { + if (activeFlows[id]) { + activeFlows[id].update(activeFlowConfig,activeFlowConfig.flows[id]); + } else { + activeFlows[id] = Flow.create(activeFlowConfig,activeFlowConfig.flows[id]); + } + } + } + } + + for (id in activeFlows) { + if (activeFlows.hasOwnProperty(id)) { + activeFlows[id].start(diff); + var activeNodes = activeFlows[id].getActiveNodes(); + Object.keys(activeNodes).forEach(function(nid) { + activeNodesToFlow[nid] = id; + }); + } + } + events.emit("nodes-started"); + if (diff) { + log.info(log._("nodes.flows.started-modified-"+type)); + } else { + log.info(log._("nodes.flows.started-flows")); + } +} + +function stop(type,diff) { + type = type||"full"; + if (diff) { + log.info(log._("nodes.flows.stopping-modified-"+type)); + } else { + log.info(log._("nodes.flows.stopping-flows")); + } + started = false; + var promises = []; + var stopList; + if (type === 'nodes') { + stopList = diff.changed.concat(diff.removed); + } else if (type === 'flows') { + stopList = diff.changed.concat(diff.removed).concat(diff.linked); + } + for (var id in activeFlows) { + if (activeFlows.hasOwnProperty(id)) { + promises = promises.concat(activeFlows[id].stop(stopList)); + if (!diff || diff.removed[id]) { + delete activeFlows[id]; + } + } + } + + return when.promise(function(resolve,reject) { + when.settle(promises).then(function() { + for (id in activeNodesToFlow) { + if (activeNodesToFlow.hasOwnProperty(id)) { + if (!activeFlows[activeNodesToFlow[id]]) { + delete activeNodesToFlow[id]; + } + } + } + if (diff) { + log.info(log._("nodes.flows.stopped-modified-"+type)); + } else { + log.info(log._("nodes.flows.stopped-flows")); + } + resolve(); + }); + }); +} + + + + +module.exports = { + init: init, + + /** + * Load the current flow configuration from storage + * @return a promise for the loading of the config + */ + load: load, + + get:getNode, + eachNode: eachNode, + + /** + * Gets the current flow configuration + */ + getFlows: getConfig, + + /** + * Sets the current active config. + * @param config the configuration to enable + * @param type the type of deployment to do: full (default), nodes, flows, load + * @return a promise for the saving/starting of the new flow + */ + setFlows: setConfig, + + /** + * Starts the current flow configuration + */ + startFlows: start, + + /** + * Stops the current flow configuration + * @return a promise for the stopping of the flow + */ + stopFlows: stop, + + + handleError: handleError, + handleStatus: handleStatus +}; diff --git a/red/nodes/flows/util.js b/red/nodes/flows/util.js new file mode 100644 index 000000000..3c561772c --- /dev/null +++ b/red/nodes/flows/util.js @@ -0,0 +1,316 @@ +/** + * Copyright 2015 IBM Corp. + * + * 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 clone = require("clone"); +var redUtil = require("../../util"); +var subflowInstanceRE = /^subflow:(.+)$/; +var typeRegistry = require("../registry"); + +function diffNodes(oldNode,newNode) { + if (oldNode == null) { + return true; + } + var oldKeys = Object.keys(oldNode).filter(function(p) { return p != "x" && p != "y" && p != "wires" }); + var newKeys = Object.keys(newNode).filter(function(p) { return p != "x" && p != "y" && p != "wires" }); + if (oldKeys.length != newKeys.length) { + return true; + } + for (var i=0;i 0) { + var subflowId = changedSubflowStack.pop(); + for (id in newConfig.allNodes) { + if (newConfig.allNodes.hasOwnProperty(id)) { + node = newConfig.allNodes[id]; + if (node.type === 'subflow:'+subflowId) { + if (!changed[node.id]) { + changed[node.id] = node; + if (!changed[changed[node.id].z] && newConfig.allNodes[changed[node.id].z]) { + changed[changed[node.id].z] = newConfig.allNodes[changed[node.id].z]; + if (newConfig.allNodes[changed[node.id].z].type === "subflow") { + // This subflow instance is inside a subflow. Add the + // containing subflow to the stack to mark + changedSubflowStack.push(changed[node.id].z); + delete changed[node.id]; + } + } + } + } + } + } + } + + var diff = { + added:Object.keys(added), + changed:Object.keys(changed), + removed:Object.keys(removed), + rewired:Object.keys(wiringChanged), + linked:[] + } + + // Traverse the links of all modified nodes to mark the connected nodes + var modifiedNodes = diff.added.concat(diff.changed).concat(diff.removed).concat(diff.rewired); + var visited = {}; + while(modifiedNodes.length > 0) { + node = modifiedNodes.pop(); + if (!visited[node]) { + visited[node] = true; + if (linkMap[node]) { + if (!changed[node] && !added[node] && !removed[node] && !wiringChanged[node]) { + diff.linked.push(node); + } + modifiedNodes = modifiedNodes.concat(linkMap[node]); + } + } + } + // for (id in newConfig.allNodes) { + // console.log( + // (added[id]?"+":(changed[id]?"!":" "))+(wiringChanged[id]?"w":" ")+(diff.linked.indexOf(id)!==-1?"~":" "), + // id, + // newConfig.allNodes[id].type, + // newConfig.allNodes[id].name||newConfig.allNodes[id].label||"" + // ); + // } + // for (id in removed) { + // console.log( + // "- "+(diff.linked.indexOf(id)!==-1?"~":" "), + // id, + // oldConfig.allNodes[id].type, + // oldConfig.allNodes[id].name||oldConfig.allNodes[id].label||"" + // ); + // } + + return diff; + } +} diff --git a/red/nodes/index.js b/red/nodes/index.js index 5a57481f6..21112be39 100644 --- a/red/nodes/index.js +++ b/red/nodes/index.js @@ -140,6 +140,7 @@ module.exports = { // Flow handling loadFlows: flows.load, + startFlows: flows.startFlows, stopFlows: flows.stopFlows, setFlows: flows.setFlows, getFlows: flows.getFlows, @@ -149,4 +150,3 @@ module.exports = { getCredentials: credentials.get, deleteCredentials: credentials.delete }; - diff --git a/red/nodes/deprecated.js b/red/nodes/registry/deprecated.js similarity index 100% rename from red/nodes/deprecated.js rename to red/nodes/registry/deprecated.js diff --git a/red/server.js b/red/server.js index db56463c5..e4d7ee277 100644 --- a/red/server.js +++ b/red/server.js @@ -107,7 +107,7 @@ function start() { } } log.info(log._("runtime.paths.settings",{path:settings.settingsFile})); - redNodes.loadFlows(); + redNodes.loadFlows().then(redNodes.startFlows); comms.start(); }).otherwise(function(err) { console.log(err); diff --git a/test/red/nodes/Flow_spec.js b/test/red/nodes/flows/Flow_spec.js similarity index 52% rename from test/red/nodes/Flow_spec.js rename to test/red/nodes/flows/Flow_spec.js index 78576dbd4..f23240550 100644 --- a/test/red/nodes/Flow_spec.js +++ b/test/red/nodes/flows/Flow_spec.js @@ -13,36 +13,36 @@ * 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 Flow = require("../../../red/nodes/Flow"); -var flows = require("../../../red/nodes/flows"); -var Node = require("../../../red/nodes/Node"); +// var Flow = require("../../../../red/nodes/Flow"); +// var flows = require("../../../../red/nodes/flows"); +// var Node = require("../../../../red/nodes/Node"); +// +// var typeRegistry = require("../../../red/nodes/registry"); +// var credentials = require("../../../red/nodes/credentials"); +return; -var typeRegistry = require("../../../red/nodes/registry"); -var credentials = require("../../../red/nodes/credentials"); - - -describe('Flow', function() { +describe.skip('Flow', function() { describe('#constructor',function() { it('called with an empty flow',function() { var config = []; var flow = new Flow(config); config.should.eql(flow.getFlow()); - + var nodeCount = 0; flow.eachNode(function(node) { nodeCount++; }); - + nodeCount.should.equal(0); }); - + it('called with a non-empty flow with no missing types', function() { var getType = sinon.stub(typeRegistry,"get",function(type) { // For this test, don't care what the actual type is, just @@ -53,13 +53,13 @@ describe('Flow', function() { var config = [{id:"123",type:"test"}]; var flow = new Flow(config); config.should.eql(flow.getFlow()); - + flow.getMissingTypes().should.have.length(0); } finally { getType.restore(); } }); - + it('identifies missing types in a flow', function() { var getType = sinon.stub(typeRegistry,"get",function(type) { if (type == "test") { @@ -72,20 +72,20 @@ describe('Flow', function() { var config = [{id:"123",type:"test"},{id:"456",type:"test1"},{id:"789",type:"test2"}]; var flow = new Flow(config); config.should.eql(flow.getFlow()); - + flow.getMissingTypes().should.eql(["test1","test2"]); } finally { getType.restore(); } }); - + it('extracts node credentials', function() { var getType = sinon.stub(typeRegistry,"get",function(type) { // For this test, don't care what the actual type is, just // that this returns a non-false result return {}; }); - + try { var config = [{id:"123",type:"test",credentials:{a:1,b:2}}]; var resultingConfig = clone(config); @@ -97,9 +97,9 @@ describe('Flow', function() { getType.restore(); } }); - + }); - + describe('missing types',function() { it('prevents a flow with missing types from starting', function() { var getType = sinon.stub(typeRegistry,"get",function(type) { @@ -113,7 +113,7 @@ describe('Flow', function() { var config = [{id:"123",type:"test"},{id:"456",type:"test1"},{id:"789",type:"test2"}]; var flow = new Flow(config); flow.getMissingTypes().should.have.length(2); - + /*jshint immed: false */ (function() { flow.start(); @@ -122,7 +122,7 @@ describe('Flow', function() { getType.restore(); } }); - + it('removes missing types as they are registered', function() { var getType = sinon.stub(typeRegistry,"get",function(type) { if (type == "test") { @@ -135,12 +135,12 @@ describe('Flow', function() { try { var config = [{id:"123",type:"test"},{id:"456",type:"test1"},{id:"789",type:"test2"}]; var flow = new Flow(config); - + flowStart = sinon.stub(flow,"start",function() {this.started = true;}); config.should.eql(flow.getFlow()); - + flow.getMissingTypes().should.eql(["test1","test2"]); - + var resp = flow.typeRegistered("a-random-node"); resp.should.be.false; @@ -148,7 +148,7 @@ describe('Flow', function() { resp.should.be.true; flow.getMissingTypes().should.eql(["test2"]); flowStart.called.should.be.false; - + resp = flow.typeRegistered("test2"); resp.should.be.true; flow.getMissingTypes().should.eql([]); @@ -158,7 +158,7 @@ describe('Flow', function() { getType.restore(); } }); - + it('starts flows once all missing types are registered', function() { var getType = sinon.stub(typeRegistry,"get",function(type) { if (type == "test") { @@ -171,19 +171,19 @@ describe('Flow', function() { try { var config = [{id:"123",type:"test"},{id:"456",type:"test1"},{id:"789",type:"test2"}]; var flow = new Flow(config); - + // First call to .start throws err due to missing types /*jshint immed: false */ (function() { flow.start(); }).should.throw(); - + // Stub .start so when missing types are registered, we don't actually try starting them flowStart = sinon.stub(flow,"start",function() {}); config.should.eql(flow.getFlow()); - + flow.getMissingTypes().should.eql(["test1","test2"]); - + flow.typeRegistered("test1"); flow.typeRegistered("test2"); flow.getMissingTypes().should.have.length(0); @@ -194,17 +194,17 @@ describe('Flow', function() { } }); }); - + describe('#start',function() { var getType; var getNode; var credentialsClean; - + var stoppedNodes = {}; var currentNodes = {}; var rewiredNodes = {}; var createCount = 0; - + var TestNode = function(n) { Node.call(this,n); createCount++; @@ -227,7 +227,7 @@ describe('Flow', function() { }; } util.inherits(TestNode,Node); - + before(function() { getNode = sinon.stub(flows,"get",function(id) { return currentNodes[id]; @@ -236,42 +236,42 @@ describe('Flow', function() { return TestNode; }); credentialsClean = sinon.stub(credentials,"clean",function(config){}); - + }); after(function() { getType.restore(); credentialsClean.restore(); getNode.restore(); }); - + beforeEach(function() { currentNodes = {}; stoppedNodes = {}; rewiredNodes = {}; createCount = 0; }); - + it("instantiates an initial configuration and stops it",function(done) { var config = [{id:"1",type:"test",foo:"a",wires:["2"]},{id:"2",type:"test",bar:"b",wires:[["3"]]},{id:"3",type:"test",foo:"a",wires:[]}]; - + var flow = new Flow(config); flow.start(); - + currentNodes.should.have.a.property("1"); currentNodes.should.have.a.property("2"); currentNodes.should.have.a.property("3"); - + currentNodes["1"].should.have.a.property("handled",0); currentNodes["2"].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["2"].should.have.a.property("handled",1); currentNodes["3"].should.have.a.property("handled",1); - + flow.stop().then(function() { currentNodes.should.not.have.a.property("1"); currentNodes.should.not.have.a.property("2"); @@ -285,7 +285,7 @@ describe('Flow', function() { it("rewires nodes specified by diff",function(done) { var config = [{id:"1",type:"test",foo:"a",wires:["2"]},{id:"2",type:"test",bar:"b",wires:[["3"]]},{id:"3",type:"test",foo:"a",wires:[]}]; - + var flow = new Flow(config); createCount.should.equal(0); flow.start(); @@ -299,15 +299,15 @@ describe('Flow', function() { }); - + describe('#stop', function() { var getType; var getNode; var credentialsClean; - + var stoppedNodes = {}; var currentNodes = {}; - + var TestNode = function(n) { Node.call(this,n); var node = this; @@ -325,7 +325,7 @@ describe('Flow', function() { }); } util.inherits(TestNode,Node); - + var TestAsyncNode = function(n) { Node.call(this,n); var node = this; @@ -346,7 +346,7 @@ describe('Flow', function() { }); } util.inherits(TestAsyncNode,Node); - + before(function() { getNode = sinon.stub(flows,"get",function(id) { return currentNodes[id]; @@ -359,28 +359,28 @@ describe('Flow', function() { } }); credentialsClean = sinon.stub(credentials,"clean",function(config){}); - + }); after(function() { getType.restore(); credentialsClean.restore(); getNode.restore(); }); - + beforeEach(function() { currentNodes = {}; stoppedNodes = {}; }); - + it("stops all nodes",function(done) { var config = [{id:"1",type:"test",foo:"a",wires:["2"]},{id:"2",type:"asyncTest",bar:"b",wires:[["3"]]},{id:"3",type:"test",foo:"a",wires:[]}]; var flow = new Flow(config); flow.start(); - + currentNodes.should.have.a.property("1"); currentNodes.should.have.a.property("2"); currentNodes.should.have.a.property("3"); - + flow.stop().then(function() { currentNodes.should.not.have.a.property("1"); currentNodes.should.not.have.a.property("2"); @@ -391,16 +391,16 @@ describe('Flow', function() { done(); }); }); - + it("stops nodes specified by diff",function(done) { var config = [{id:"1",type:"test",foo:"a",wires:["2"]},{id:"2",type:"test",bar:"b",wires:[["3"]]},{id:"3",type:"test",foo:"a",wires:[]}]; var flow = new Flow(config); flow.start(); - + currentNodes.should.have.a.property("1"); currentNodes.should.have.a.property("2"); currentNodes.should.have.a.property("3"); - + flow.stop({stop:["2"]}).then(function() { currentNodes.should.have.a.property("1"); currentNodes.should.not.have.a.property("2"); @@ -414,15 +414,15 @@ describe('Flow', function() { }); - + describe('#diffConfig',function() { var getType; var getNode; var credentialsClean; - + var stoppedNodes = {}; var currentNodes = {}; - + var TestNode = function(n) { Node.call(this,n); var node = this; @@ -440,7 +440,7 @@ describe('Flow', function() { }); } util.inherits(TestNode,Node); - + before(function() { getNode = sinon.stub(flows,"get",function(id) { return currentNodes[id]; @@ -449,590 +449,23 @@ describe('Flow', function() { return TestNode; }); credentialsClean = sinon.stub(credentials,"clean",function(config){}); - + }); after(function() { getType.restore(); credentialsClean.restore(); getNode.restore(); }); - + beforeEach(function() { currentNodes = {}; stoppedNodes = {}; }); - - - it('handles an identical configuration', function() { - var config = [{id:"123",type:"test",foo:"a",wires:[]}]; - var flow = new Flow(config); - flow.start(); - flow.getMissingTypes().should.have.length(0); - - var diffResult = flow.diffConfig(config,"full"); - diffResult.should.have.property("config",config); - diffResult.should.have.property("type","full"); - diffResult.should.have.property("stop",["123"]); - diffResult.should.have.property("rewire",[]); - - diffResult = flow.diffConfig(config,"nodes"); - diffResult.should.have.property("config",config); - diffResult.should.have.property("type","nodes"); - diffResult.should.have.property("stop",[]); - diffResult.should.have.property("rewire",[]); - - diffResult = flow.diffConfig(config,"flows"); - diffResult.should.have.property("config",config); - diffResult.should.have.property("type","flows"); - diffResult.should.have.property("stop",[]); - diffResult.should.have.property("rewire",[]); - }); - - it('identifies nodes with changed properties, including downstream linked', function() { - var config = [{id:"1",type:"test",foo:"a",wires:[]},{id:"2",type:"test",bar:"b",wires:[[1]]},{id:"3",type:"test",foo:"a",wires:[]}]; - var newConfig = clone(config); - newConfig[0].foo = "b"; - - var flow = new Flow(config); - flow.start(); - flow.getMissingTypes().should.have.length(0); - - var diffResult = flow.diffConfig(newConfig,"full"); - diffResult.should.have.property("config",newConfig); - diffResult.should.have.property("type","full"); - diffResult.stop.sort().should.eql(["1","2","3"]); - diffResult.should.have.property("rewire",[]); - - diffResult = flow.diffConfig(newConfig,"nodes"); - diffResult.should.have.property("config",newConfig); - diffResult.should.have.property("type","nodes"); - diffResult.should.have.property("stop",["1"]); - diffResult.should.have.property("rewire",[]); - - diffResult = flow.diffConfig(newConfig,"flows"); - diffResult.should.have.property("type","flows"); - diffResult.should.have.property("stop",["1","2"]); - diffResult.should.have.property("rewire",[]); - diffResult.should.have.property("config",newConfig); - }); - - it('identifies nodes with changed properties, including upstream linked', function() { - var config = [{id:"1",type:"test",foo:"a",wires:[]},{id:"2",type:"test",bar:"b",wires:[[1]]},{id:"3",type:"test",foo:"a",wires:[]}]; - var newConfig = clone(config); - newConfig[1].bar = "c"; - - var flow = new Flow(config); - flow.start(); - flow.getMissingTypes().should.have.length(0); - - var diffResult = flow.diffConfig(newConfig,"full"); - diffResult.should.have.property("config",newConfig); - diffResult.should.have.property("type","full"); - diffResult.stop.sort().should.eql(["1","2","3"]); - diffResult.should.have.property("rewire",[]); - - diffResult = flow.diffConfig(newConfig,"nodes"); - diffResult.should.have.property("config",newConfig); - diffResult.should.have.property("type","nodes"); - diffResult.should.have.property("stop",["2"]); - diffResult.should.have.property("rewire",[]); - - diffResult = flow.diffConfig(newConfig,"flows"); - diffResult.should.have.property("type","flows"); - diffResult.stop.sort().should.eql(["1","2"]); - diffResult.should.have.property("rewire",[]); - diffResult.should.have.property("config",newConfig); - }); - - it('identifies nodes with changed credentials, including downstream linked', function() { - var config = [{id:"1",type:"test",wires:[]},{id:"2",type:"test",bar:"b",wires:[[1]]},{id:"3",type:"test",foo:"a",wires:[]}]; - var newConfig = clone(config); - newConfig[0].credentials = {}; - - var flow = new Flow(config); - flow.start(); - flow.getMissingTypes().should.have.length(0); - - var diffResult = flow.diffConfig(newConfig,"full"); - diffResult.should.have.property("config",newConfig); - diffResult.should.have.property("type","full"); - diffResult.stop.sort().should.eql(["1","2","3"]); - diffResult.should.have.property("rewire",[]); - - newConfig[0].credentials = {}; - diffResult = flow.diffConfig(newConfig,"nodes"); - diffResult.should.have.property("config",newConfig); - diffResult.should.have.property("type","nodes"); - diffResult.should.have.property("stop",["1"]); - diffResult.should.have.property("rewire",[]); - - newConfig[0].credentials = {}; - diffResult = flow.diffConfig(newConfig,"flows"); - diffResult.should.have.property("type","flows"); - diffResult.stop.sort().should.eql(["1","2"]); - diffResult.should.have.property("rewire",[]); - diffResult.should.have.property("config",newConfig); - }); - - it('identifies nodes with changed wiring', function() { - var config = [{id:"1",type:"test",foo:"a",wires:[]},{id:"2",type:"test",bar:"b",wires:[[1]]},{id:"3",type:"test",foo:"a",wires:[]}]; - var newConfig = clone(config); - newConfig[1].wires[0][0] = 2; - - var flow = new Flow(config); - flow.start(); - flow.getMissingTypes().should.have.length(0); - - var diffResult = flow.diffConfig(newConfig,"full"); - diffResult.should.have.property("config",newConfig); - diffResult.should.have.property("type","full"); - diffResult.stop.sort().should.eql(["1","2","3"]); - diffResult.should.have.property("rewire",[]); - - diffResult = flow.diffConfig(newConfig,"nodes"); - diffResult.should.have.property("config",newConfig); - diffResult.should.have.property("type","nodes"); - diffResult.should.have.property("stop",[]); - diffResult.should.have.property("rewire",["2"]); - - diffResult = flow.diffConfig(newConfig,"flows"); - diffResult.should.have.property("type","flows"); - diffResult.stop.sort().should.eql(["1","2"]); - diffResult.should.have.property("rewire",["2"]); - diffResult.should.have.property("config",newConfig); - }); - - it('identifies nodes with changed wiring - second connection added', function() { - var config = [{id:"1",type:"test",foo:"a",wires:[]},{id:"2",type:"test",bar:"b",wires:[[1]]},{id:"3",type:"test",foo:"a",wires:[]}]; - var newConfig = clone(config); - newConfig[1].wires[0].push([1]); - - var flow = new Flow(config); - flow.start(); - flow.getMissingTypes().should.have.length(0); - - var diffResult = flow.diffConfig(newConfig,"full"); - diffResult.should.have.property("config",newConfig); - diffResult.should.have.property("type","full"); - diffResult.stop.sort().should.eql(["1","2","3"]); - diffResult.should.have.property("rewire",[]); - - diffResult = flow.diffConfig(newConfig,"nodes"); - diffResult.should.have.property("config",newConfig); - diffResult.should.have.property("type","nodes"); - diffResult.should.have.property("stop",[]); - diffResult.should.have.property("rewire",["2"]); - - diffResult = flow.diffConfig(newConfig,"flows"); - diffResult.should.have.property("type","flows"); - diffResult.stop.sort().should.eql(["1","2"]); - diffResult.should.have.property("rewire",["2"]); - diffResult.should.have.property("config",newConfig); - }); - - it('identifies nodes with changed wiring - second connection added', function() { - var config = [{id:"1",type:"test",foo:"a",wires:[]},{id:"2",type:"test",bar:"b",wires:[[1]]},{id:"3",type:"test",foo:"a",wires:[]}]; - var newConfig = clone(config); - newConfig[1].wires[0].push([1]); - - var flow = new Flow(config); - flow.start(); - flow.getMissingTypes().should.have.length(0); - - var diffResult = flow.diffConfig(newConfig,"full"); - diffResult.should.have.property("config",newConfig); - diffResult.should.have.property("type","full"); - diffResult.stop.sort().should.eql(["1","2","3"]); - diffResult.should.have.property("rewire",[]); - - diffResult = flow.diffConfig(newConfig,"nodes"); - diffResult.should.have.property("config",newConfig); - diffResult.should.have.property("type","nodes"); - diffResult.should.have.property("stop",[]); - diffResult.should.have.property("rewire",["2"]); - - diffResult = flow.diffConfig(newConfig,"flows"); - diffResult.should.have.property("type","flows"); - diffResult.stop.sort().should.eql(["1","2"]); - diffResult.should.have.property("rewire",["2"]); - diffResult.should.have.property("config",newConfig); - - }); - - it('identifies nodes with changed wiring - node connected', function() { - var config = [{id:"1",type:"test",foo:"a",wires:[["2"]]},{id:"2",type:"test",bar:"b",wires:[[]]},{id:"3",type:"test",foo:"a",wires:[]}]; - var newConfig = clone(config); - newConfig[1].wires.push("3"); - - var flow = new Flow(config); - flow.start(); - flow.getMissingTypes().should.have.length(0); - - var diffResult = flow.diffConfig(newConfig,"full"); - diffResult.should.have.property("config",newConfig); - diffResult.should.have.property("type","full"); - diffResult.stop.sort().should.eql(["1","2","3"]); - diffResult.should.have.property("rewire",[]); - - diffResult = flow.diffConfig(newConfig,"nodes"); - diffResult.should.have.property("config",newConfig); - diffResult.should.have.property("type","nodes"); - diffResult.should.have.property("stop",[]); - diffResult.should.have.property("rewire",["2"]); - - diffResult = flow.diffConfig(newConfig,"flows"); - diffResult.should.have.property("type","flows"); - diffResult.stop.sort().should.eql(["1","2","3"]); - diffResult.should.have.property("rewire",["2"]); - diffResult.should.have.property("config",newConfig); - }); - - it('identifies new nodes', function() { - var config = [{id:"1",type:"test",foo:"a",wires:[]},{id:"3",type:"test",foo:"a",wires:[]}]; - var newConfig = clone(config); - newConfig.push({id:"2",type:"test",bar:"b",wires:[[1]]}); - - var flow = new Flow(config); - flow.start(); - flow.getMissingTypes().should.have.length(0); - - var diffResult = flow.diffConfig(newConfig,"full"); - diffResult.should.have.property("config",newConfig); - diffResult.should.have.property("type","full"); - diffResult.stop.sort().should.eql(["1","3"]); - diffResult.should.have.property("rewire",[]); - - diffResult = flow.diffConfig(newConfig,"nodes"); - diffResult.should.have.property("config",newConfig); - diffResult.should.have.property("type","nodes"); - diffResult.should.have.property("stop",[]); - diffResult.should.have.property("rewire",[]); - - diffResult = flow.diffConfig(newConfig,"flows"); - diffResult.should.have.property("type","flows"); - diffResult.stop.sort().should.eql(["1"]); - diffResult.should.have.property("rewire",[]); - diffResult.should.have.property("config",newConfig); - }); - - it('identifies deleted nodes', function() { - var config = [{id:"1",type:"test",foo:"a",wires:[[2]]},{id:"2",type:"test",bar:"b",wires:[[3]]},{id:"3",type:"test",foo:"a",wires:[]}]; - var newConfig = clone(config); - newConfig.splice(1,1); - newConfig[0].wires = []; - - var flow = new Flow(config); - flow.start(); - flow.getMissingTypes().should.have.length(0); - - var diffResult = flow.diffConfig(newConfig,"full"); - diffResult.should.have.property("config",newConfig); - diffResult.should.have.property("type","full"); - diffResult.stop.sort().should.eql(["1","2","3"]); - diffResult.should.have.property("rewire",[]); - - diffResult = flow.diffConfig(newConfig,"nodes"); - diffResult.should.have.property("config",newConfig); - diffResult.should.have.property("type","nodes"); - diffResult.should.have.property("stop",["2"]); - diffResult.should.have.property("rewire",["1"]); - - diffResult = flow.diffConfig(newConfig,"flows"); - diffResult.should.have.property("type","flows"); - diffResult.stop.sort().should.eql(["1","2","3"]); - diffResult.should.have.property("rewire",["1"]); - diffResult.should.have.property("config",newConfig); - - - }); - - it('identifies config nodes changes', function() { - var config = [{id:"1",type:"test",foo:"configNode",wires:[[2]]},{id:"2",type:"test",bar:"b",wires:[[3]]},{id:"3",type:"test",foo:"a",wires:[]},{id:"configNode",type:"testConfig"}]; - var newConfig = clone(config); - newConfig[3].foo = "bar"; - - var flow = new Flow(config); - flow.start(); - flow.getMissingTypes().should.have.length(0); - - var diffResult = flow.diffConfig(newConfig,"full"); - diffResult.should.have.property("config",newConfig); - diffResult.should.have.property("type","full"); - diffResult.stop.sort().should.eql(["1","2","3","configNode"]); - diffResult.should.have.property("rewire",[]); - - diffResult = flow.diffConfig(newConfig,"nodes"); - diffResult.should.have.property("config",newConfig); - diffResult.should.have.property("type","nodes"); - diffResult.should.have.property("stop",["1","configNode"]); - diffResult.should.have.property("rewire",[]); - - diffResult = flow.diffConfig(newConfig,"flows"); - diffResult.should.have.property("type","flows"); - diffResult.stop.sort().should.eql(["1","2","3","configNode"]); - diffResult.should.have.property("rewire",[]); - diffResult.should.have.property("config",newConfig); - - - }); - - it('marks a parent subflow as changed for an internal property change', function() { - var config = [ - {id:"1",type:"test",wires:[[2]]}, - {id:"2",type:"subflow:sf1",wires:[[3]]}, - {id:"3",type:"test",wires:[]}, - {id:"sf1",type:"subflow"}, - {id:"sf1-1",z:"sf1",type:"test",foo:"a",wires:[]}, - {id:"4",type:"subflow:sf1",wires:[]} - ]; - - var newConfig = clone(config); - newConfig[4].foo = "b"; - - var flow = new Flow(config); - flow.start(); - flow.getMissingTypes().should.have.length(0); - - var diffResult = flow.diffConfig(newConfig,"full"); - diffResult.should.have.property("config",newConfig); - diffResult.should.have.property("type","full"); - diffResult.should.have.property("stop").with.lengthOf(6); - diffResult.should.have.property("rewire",[]); - - diffResult = flow.diffConfig(newConfig,"nodes"); - diffResult.should.have.property("config",newConfig); - diffResult.should.have.property("type","nodes"); - diffResult.should.have.property("stop").with.lengthOf(4); - diffResult.should.have.property("rewire",[]); - - diffResult = flow.diffConfig(newConfig,"flows"); - diffResult.should.have.property("type","flows"); - diffResult.should.have.property("stop").with.lengthOf(6); - diffResult.should.have.property("rewire",[]); - diffResult.should.have.property("config",newConfig); - - }); - - it('marks a parent subflow as changed for an internal wiring change', function() { - var config = [ - {id:"1",type:"test",wires:[[2]]}, - {id:"2",type:"subflow:sf1",wires:[[3]]}, - {id:"3",type:"test",wires:[]}, - {id:"sf1",type:"subflow"}, - {id:"sf1-1",z:"sf1",type:"test",wires:[]}, - {id:"sf1-2",z:"sf1",type:"test",wires:[]} - ]; - - var newConfig = clone(config); - newConfig[4].wires = [["sf1-2"]]; - - var flow = new Flow(config); - flow.start(); - flow.getMissingTypes().should.have.length(0); - - var diffResult = flow.diffConfig(newConfig,"full"); - diffResult.should.have.property("config",newConfig); - diffResult.should.have.property("type","full"); - diffResult.should.have.property("stop").with.lengthOf(5); - diffResult.should.have.property("rewire",[]); - - diffResult = flow.diffConfig(newConfig,"nodes"); - diffResult.should.have.property("config",newConfig); - diffResult.should.have.property("type","nodes"); - diffResult.should.have.property("stop").with.lengthOf(3); - diffResult.should.have.property("rewire",[]); - - diffResult = flow.diffConfig(newConfig,"flows"); - diffResult.should.have.property("type","flows"); - diffResult.should.have.property("stop").with.lengthOf(5); - diffResult.should.have.property("rewire",[]); - diffResult.should.have.property("config",newConfig); - }); - it('marks a parent subflow as changed for an internal node delete', function() { - var config = [ - {id:"1",type:"test",wires:[[2]]}, - {id:"2",type:"subflow:sf1",wires:[[3]]}, - {id:"3",type:"test",wires:[]}, - {id:"sf1",type:"subflow"}, - {id:"sf1-1",z:"sf1",type:"test",wires:[]}, - {id:"sf1-2",z:"sf1",type:"test",wires:[]} - ]; - - var newConfig = clone(config); - newConfig.splice(5,1); - - var flow = new Flow(config); - flow.start(); - flow.getMissingTypes().should.have.length(0); - - var diffResult = flow.diffConfig(newConfig,"full"); - diffResult.should.have.property("config",newConfig); - diffResult.should.have.property("type","full"); - diffResult.should.have.property("stop").with.lengthOf(5); - diffResult.should.have.property("rewire",[]); - - diffResult = flow.diffConfig(newConfig,"nodes"); - diffResult.should.have.property("config",newConfig); - diffResult.should.have.property("type","nodes"); - diffResult.should.have.property("stop").with.lengthOf(3); - diffResult.should.have.property("rewire",[]); - - diffResult = flow.diffConfig(newConfig,"flows"); - diffResult.should.have.property("type","flows"); - diffResult.should.have.property("stop").with.lengthOf(5); - diffResult.should.have.property("rewire",[]); - diffResult.should.have.property("config",newConfig); - - }); - it('marks a parent subflow as changed for an internal subflow wiring change - input removed', function() { - var config = [ - {id:"1",type:"test",wires:[[2]]}, - {id:"2",type:"subflow:sf1",wires:[[3]]}, - {id:"3",type:"test",wires:[]}, - {id:"sf1",type:"subflow","in": [{"wires": [{"id": "sf1-1"}]}],"out": [{"wires": [{"id": "sf1-2","port": 0}]}]}, - {id:"sf1-1",z:"sf1",type:"test",wires:[]}, - {id:"sf1-2",z:"sf1",type:"test",wires:[]} - ]; - - var newConfig = clone(config); - newConfig[3].in[0].wires = []; - - var flow = new Flow(config); - flow.start(); - flow.getMissingTypes().should.have.length(0); - - var diffResult = flow.diffConfig(newConfig,"full"); - diffResult.should.have.property("config",newConfig); - diffResult.should.have.property("type","full"); - diffResult.should.have.property("stop").with.lengthOf(5); - diffResult.should.have.property("rewire",[]); - - diffResult = flow.diffConfig(newConfig,"nodes"); - diffResult.should.have.property("config",newConfig); - diffResult.should.have.property("type","nodes"); - diffResult.should.have.property("stop").with.lengthOf(3); - diffResult.should.have.property("rewire",[]); - - diffResult = flow.diffConfig(newConfig,"flows"); - diffResult.should.have.property("type","flows"); - diffResult.should.have.property("stop").with.lengthOf(5); - diffResult.should.have.property("rewire",[]); - diffResult.should.have.property("config",newConfig); - }); - - it('marks a parent subflow as changed for an internal subflow wiring change - input added', function() { - var config = [ - {id:"1",type:"test",wires:[[2]]}, - {id:"2",type:"subflow:sf1",wires:[[3]]}, - {id:"3",type:"test",wires:[]}, - {id:"sf1",type:"subflow","in": [{"wires": [{"id": "sf1-1"}]}],"out": [{"wires": [{"id": "sf1-2","port": 0}]}]}, - {id:"sf1-1",z:"sf1",type:"test",wires:[]}, - {id:"sf1-2",z:"sf1",type:"test",wires:[]} - ]; - - var newConfig = clone(config); - newConfig[3].in[0].wires.push({"id":"sf1-2"}); - - var flow = new Flow(config); - flow.start(); - flow.getMissingTypes().should.have.length(0); - - var diffResult = flow.diffConfig(newConfig,"full"); - diffResult.should.have.property("config",newConfig); - diffResult.should.have.property("type","full"); - diffResult.should.have.property("stop").with.lengthOf(5); - diffResult.should.have.property("rewire",[]); - - diffResult = flow.diffConfig(newConfig,"nodes"); - diffResult.should.have.property("config",newConfig); - diffResult.should.have.property("type","nodes"); - diffResult.should.have.property("stop").with.lengthOf(3); - diffResult.should.have.property("rewire",[]); - - diffResult = flow.diffConfig(newConfig,"flows"); - diffResult.should.have.property("type","flows"); - diffResult.should.have.property("stop").with.lengthOf(5); - diffResult.should.have.property("rewire",[]); - diffResult.should.have.property("config",newConfig); - }); - - it('marks a parent subflow as changed for an internal subflow wiring change - output added', function() { - var config = [ - {id:"1",type:"test",wires:[[2]]}, - {id:"2",type:"subflow:sf1",wires:[[3]]}, - {id:"3",type:"test",wires:[]}, - {id:"sf1",type:"subflow","in": [{"wires": [{"id": "sf1-1"}]}],"out": [{"wires": [{"id": "sf1-2","port": 0}]}]}, - {id:"sf1-1",z:"sf1",type:"test",wires:[]}, - {id:"sf1-2",z:"sf1",type:"test",wires:[]} - ]; - - var newConfig = clone(config); - newConfig[3].out[0].wires.push({"id":"sf1-2","port":0}); - - var flow = new Flow(config); - flow.start(); - flow.getMissingTypes().should.have.length(0); - - var diffResult = flow.diffConfig(newConfig,"full"); - diffResult.should.have.property("config",newConfig); - diffResult.should.have.property("type","full"); - diffResult.should.have.property("stop").with.lengthOf(5); - diffResult.should.have.property("rewire",[]); - - diffResult = flow.diffConfig(newConfig,"nodes"); - diffResult.should.have.property("config",newConfig); - diffResult.should.have.property("type","nodes"); - diffResult.should.have.property("stop").with.lengthOf(3); - diffResult.should.have.property("rewire",[]); - - diffResult = flow.diffConfig(newConfig,"flows"); - diffResult.should.have.property("type","flows"); - diffResult.should.have.property("stop").with.lengthOf(5); - diffResult.should.have.property("rewire",[]); - diffResult.should.have.property("config",newConfig); - }); - - it('marks a parent subflow as changed for an internal subflow wiring change - output removed', function() { - var config = [ - {id:"1",type:"test",wires:[[2]]}, - {id:"2",type:"subflow:sf1",wires:[[3]]}, - {id:"3",type:"test",wires:[]}, - {id:"sf1",type:"subflow","in": [{"wires": [{"id": "sf1-1"}]}],"out": [{"wires": [{"id": "sf1-2","port": 0}]}]}, - {id:"sf1-1",z:"sf1",type:"test",wires:[]}, - {id:"sf1-2",z:"sf1",type:"test",wires:[]} - ]; - - var newConfig = clone(config); - newConfig[3].out[0].wires = []; - - var flow = new Flow(config); - flow.start(); - flow.getMissingTypes().should.have.length(0); - - var diffResult = flow.diffConfig(newConfig,"full"); - diffResult.should.have.property("config",newConfig); - diffResult.should.have.property("type","full"); - diffResult.should.have.property("stop").with.lengthOf(5); - diffResult.should.have.property("rewire",[]); - - diffResult = flow.diffConfig(newConfig,"nodes"); - diffResult.should.have.property("config",newConfig); - diffResult.should.have.property("type","nodes"); - diffResult.should.have.property("stop").with.lengthOf(3); - diffResult.should.have.property("rewire",[]); - - diffResult = flow.diffConfig(newConfig,"flows"); - diffResult.should.have.property("type","flows"); - diffResult.should.have.property("stop").with.lengthOf(5); - diffResult.should.have.property("rewire",[]); - diffResult.should.have.property("config",newConfig); - }); - - + }); describe("#diffConfig",function() { @@ -1040,10 +473,10 @@ describe('Flow', function() { var getType; var getNode; var credentialsClean; - + var stoppedNodes = {}; var currentNodes = {}; - + var TestNode = function(n) { Node.call(this,n); var node = this; @@ -1061,7 +494,7 @@ describe('Flow', function() { }); } util.inherits(TestNode,Node); - + before(function() { getNode = sinon.stub(flows,"get",function(id) { return currentNodes[id]; @@ -1070,50 +503,50 @@ describe('Flow', function() { return TestNode; }); credentialsClean = sinon.stub(credentials,"clean",function(config){}); - + }); after(function() { getType.restore(); credentialsClean.restore(); getNode.restore(); }); - + beforeEach(function() { currentNodes = {}; stoppedNodes = {}; }); - + it('handles an identical configuration', function() { var config = [{id:"123",type:"test",foo:"a",wires:[]}]; var newConfig = [{id:"123",type:"test",foo:"a",wires:[]}]; - + var flow = new Flow(config); flow.start(); var diffResult = flow.diffConfig(newConfig,"nodes"); - + diffResult.should.have.property("type","nodes"); diffResult.should.have.property("stop",[]); diffResult.should.have.property("rewire",[]); diffResult.should.have.property("config",newConfig); }); - - + + }); - - + + describe("#dead",function() { return; it("stops all nodes on new full deploy",function(done) { var config = [{id:"1",type:"test",foo:"a",wires:["2"]},{id:"2",type:"test",bar:"b",wires:[["3"]]},{id:"3",type:"test",foo:"a",wires:[]}]; var newConfig = [{id:"4",type:"test",foo:"a",wires:["5"]},{id:"5",type:"test",bar:"b",wires:[["6"]]},{id:"6",type:"test",foo:"a",wires:[]}]; - + var flow = new Flow(config); flow.start(); - + currentNodes.should.have.a.property("1"); currentNodes.should.have.a.property("2"); currentNodes.should.have.a.property("3"); - + flow.applyConfig(newConfig).then(function() { currentNodes.should.not.have.a.property("1"); currentNodes.should.not.have.a.property("2"); @@ -1121,7 +554,7 @@ describe('Flow', function() { stoppedNodes.should.have.a.property("1"); stoppedNodes.should.have.a.property("2"); stoppedNodes.should.have.a.property("3"); - + currentNodes.should.have.a.property("4"); currentNodes.should.have.a.property("5"); currentNodes.should.have.a.property("6"); @@ -1129,15 +562,15 @@ describe('Flow', function() { }); }); return true; - + it("stops only modified nodes on 'nodes' deploy",function(done) { var config = [{id:"1",type:"test",name:"a",wires:["2"]},{id:"2",type:"test",name:"b",wires:[["3"]]},{id:"3",type:"test",name:"c",wires:[]}]; var newConfig = clone(config); newConfig[1].name = "B"; - + var flow = new Flow(config); flow.start(); - + currentNodes.should.have.a.property("1"); currentNodes.should.have.a.property("2"); currentNodes.should.have.a.property("3"); @@ -1148,19 +581,19 @@ describe('Flow', function() { currentNodes["2"].should.have.a.property("handled",1); currentNodes["3"].should.have.a.property("handled",1); - + flow.applyConfig(newConfig,"nodes").then(function() { currentNodes.should.have.a.property("1"); currentNodes.should.have.a.property("2"); currentNodes.should.have.a.property("3"); currentNodes["2"].should.have.a.property("name","B"); - + stoppedNodes.should.not.have.a.property("1"); stoppedNodes.should.have.a.property("2"); stoppedNodes.should.not.have.a.property("3"); stoppedNodes["2"].should.have.a.property("name","b"); - - + + currentNodes["1"].receive({payload:"test"}); currentNodes["1"].should.have.a.property("handled",2); currentNodes["2"].should.have.a.property("handled",1); @@ -1174,77 +607,77 @@ describe('Flow', function() { var config = [{id:"1",type:"test",name:"a",wires:["2"]},{id:"2",type:"test",name:"b",wires:[[]]},{id:"3",type:"test",name:"c",wires:[]}]; var newConfig = clone(config); newConfig[1].name = "B"; - + var flow = new Flow(config); flow.start(); - + currentNodes.should.have.a.property("1"); currentNodes.should.have.a.property("2"); currentNodes.should.have.a.property("3"); currentNodes["2"].should.have.a.property("name","b"); - + currentNodes["1"].receive({payload:"test"}); currentNodes["1"].should.have.a.property("handled",1); currentNodes["2"].should.have.a.property("handled",1); currentNodes["3"].should.have.a.property("handled",0); - + currentNodes["3"].receive({payload:"test"}); currentNodes["3"].should.have.a.property("handled",1); - + flow.applyConfig(newConfig,"flows").then(function() { currentNodes.should.have.a.property("1"); currentNodes.should.have.a.property("2"); currentNodes.should.have.a.property("3"); currentNodes["2"].should.have.a.property("name","B"); - + stoppedNodes.should.have.a.property("1"); stoppedNodes.should.have.a.property("2"); stoppedNodes.should.not.have.a.property("3"); - + stoppedNodes["2"].should.have.a.property("name","b"); - + currentNodes["1"].receive({payload:"test"}); currentNodes["1"].should.have.a.property("handled",1); currentNodes["2"].should.have.a.property("handled",1); - + currentNodes["3"].receive({payload:"test"}); currentNodes["3"].should.have.a.property("handled",2); - + done(); }); }); - + it("rewires otherwise unmodified nodes on 'nodes' deploy",function(done) { var config = [{id:"1",type:"test",name:"a",wires:["2"]},{id:"2",type:"test",name:"b",wires:[[]]},{id:"3",type:"test",name:"c",wires:[]}]; var newConfig = clone(config); newConfig[1].wires[0].push("3"); - + var flow = new Flow(config); flow.start(); - + currentNodes.should.have.a.property("1"); currentNodes.should.have.a.property("2"); currentNodes.should.have.a.property("3"); - + currentNodes["1"].receive({payload:"test"}); currentNodes["1"].should.have.a.property("handled",1); currentNodes["2"].should.have.a.property("handled",1); currentNodes["3"].should.have.a.property("handled",0); - + flow.applyConfig(newConfig,"nodes").then(function() { currentNodes.should.have.a.property("1"); currentNodes.should.have.a.property("2"); currentNodes.should.have.a.property("3"); - + stoppedNodes.should.not.have.a.property("1"); stoppedNodes.should.not.have.a.property("2"); stoppedNodes.should.not.have.a.property("3"); - + currentNodes["1"].receive({payload:"test"}); currentNodes["1"].should.have.a.property("handled",2); currentNodes["2"].should.have.a.property("handled",2); currentNodes["3"].should.have.a.property("handled",1); - + done(); }); }); @@ -1253,45 +686,45 @@ describe('Flow', function() { var config = [{id:"1",type:"test",name:"a",wires:["2"]},{id:"2",type:"test",name:"b",wires:[[]]},{id:"3",type:"test",name:"c",wires:[]}]; var newConfig = clone(config); newConfig[1].wires[0].push("3"); - + var flow = new Flow(config); flow.start(); - + currentNodes.should.have.a.property("1"); currentNodes.should.have.a.property("2"); currentNodes.should.have.a.property("3"); - + currentNodes["1"].receive({payload:"test"}); currentNodes["1"].should.have.a.property("handled",1); currentNodes["2"].should.have.a.property("handled",1); currentNodes["3"].should.have.a.property("handled",0); - + flow.applyConfig(newConfig,"flows").then(function() { currentNodes.should.have.a.property("1"); currentNodes.should.have.a.property("2"); currentNodes.should.have.a.property("3"); - + stoppedNodes.should.have.a.property("1"); stoppedNodes.should.have.a.property("2"); stoppedNodes.should.have.a.property("3"); - + currentNodes["1"].receive({payload:"test"}); currentNodes["1"].should.have.a.property("handled",1); currentNodes["2"].should.have.a.property("handled",1); currentNodes["3"].should.have.a.property("handled",1); - + done(); }); }); }); - + describe('#handleError',function() { var getType; var getNode; var credentialsClean; - + var currentNodes = {}; - + var TestNode = function(n) { Node.call(this,n); var node = this; @@ -1303,7 +736,7 @@ describe('Flow', function() { }); } util.inherits(TestNode,Node); - + before(function() { getNode = sinon.stub(flows,"get",function(id) { return currentNodes[id]; @@ -1312,18 +745,18 @@ describe('Flow', function() { return TestNode; }); credentialsClean = sinon.stub(credentials,"clean",function(config){}); - + }); after(function() { getType.restore(); credentialsClean.restore(); getNode.restore(); }); - + beforeEach(function() { currentNodes = {}; }); - + it("reports error to catch nodes on same z",function(done) { var config = [ {id:"1",type:"test",z:"tab1",name:"a",wires:["2"]}, @@ -1345,7 +778,7 @@ describe('Flow', function() { getNode(3).handled.should.have.lengthOf(0); done(); }); - + it("reports error with Error object",function(done) { var config = [ {id:"1",type:"test",z:"tab1",name:"a",wires:["2"]}, @@ -1367,7 +800,7 @@ describe('Flow', function() { getNode(3).handled.should.have.lengthOf(0); done(); }); - + it("reports error with Error object",function(done) { var config = [ {id:"1",type:"test",z:"tab1",name:"a",wires:["2"]}, @@ -1387,7 +820,7 @@ describe('Flow', function() { getNode(3).handled.should.have.lengthOf(0); done(); }); - + it('reports error in subflow to a local handler', function(done) { var config = [ {id:"1",type:"test",z:"tab1",wires:[[]]}, @@ -1422,7 +855,7 @@ describe('Flow', function() { n3.handled[0].error.source.should.have.property("type","test"); getNode(3).handled.should.have.lengthOf(0); done(); - }); + }); it('reports error in subflow to a parent handler', function(done) { var config = [ {id:"1",type:"test",z:"tab1",wires:[[]]}, @@ -1435,7 +868,7 @@ describe('Flow', function() { var flow = new Flow(config); flow.start(); var instanceNode; - + for (var id in currentNodes) { if (currentNodes.hasOwnProperty(id)) { if (currentNodes[id].z == '2') { @@ -1455,4 +888,4 @@ describe('Flow', function() { done(); }); }); -}); \ No newline at end of file +}); diff --git a/test/red/nodes/flows/index_spec.js b/test/red/nodes/flows/index_spec.js new file mode 100644 index 000000000..1276de5ad --- /dev/null +++ b/test/red/nodes/flows/index_spec.js @@ -0,0 +1,415 @@ +/** + * Copyright 2014, 2015 IBM Corp. + * + * 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 when = require("when"); +var clone = require("clone"); +var flows = require("../../../../red/nodes/flows"); +var RedNode = require("../../../../red/nodes/Node"); +var RED = require("../../../../red/nodes"); +var events = require("../../../../red/events"); +var credentials = require("../../../../red/nodes/credentials"); +var typeRegistry = require("../../../../red/nodes/registry"); +var Flow = require("../../../../red/nodes/flows/Flow"); + +var settings = { + available: function() { return false; } +} + +function loadFlows(testFlows, cb) { + var storage = { + getFlows: function() { + return when.resolve(testFlows); + }, + getCredentials: function() { + return when.resolve({}); + } + }; + RED.init(settings, storage); + flows.load().then(function() { + should.deepEqual(testFlows, flows.getFlows()); + cb(); + }); +} + +describe('flows/index', function() { + + var eventsOn; + var storage; + var credentialsExtract; + var credentialsSave; + var credentialsClean; + var credentialsLoad; + + var flowCreate; + var getType; + + before(function() { + getType = sinon.stub(typeRegistry,"get",function(type) { + return type!=='missing'; + }); + }); + after(function() { + getType.restore(); + }); + + + beforeEach(function() { + eventsOn = sinon.stub(events,"on",function(evt,handler) {}); + credentialsExtract = sinon.stub(credentials,"extract",function(conf) { + delete conf.credentials; + }); + credentialsSave = sinon.stub(credentials,"save",function() { + return when.resolve(); + }); + credentialsClean = sinon.stub(credentials,"clean",function(conf) { + return when.resolve(); + }); + credentialsLoad = sinon.stub(credentials,"load",function() { + return when.resolve(); + }); + flowCreate = sinon.stub(Flow,"create",function(global, flow) { + var id; + if (typeof flow === 'undefined') { + flow = global; + id = '_GLOBAL_'; + } else { + id = flow.id; + } + flowCreate.flows[id] = { + start: sinon.spy(), + update: sinon.spy(), + stop: sinon.spy() + } + return flowCreate.flows[id]; + }); + flowCreate.flows = {}; + + storage = { + saveFlows: function(conf) { + storage.conf = conf; + return when.resolve(); + } + } + }); + + afterEach(function() { + eventsOn.restore(); + credentialsExtract.restore(); + credentialsSave.restore(); + credentialsClean.restore(); + credentialsLoad.restore(); + flowCreate.restore(); + }); + describe('#init',function() { + it('registers the type-registered handler', function() { + flows.init({},{}); + eventsOn.calledOnce.should.be.true; + }); + }); + + describe('#setFlows', function() { + it('sets the full flow', function(done) { + var originalConfig = [ + {id:"t1-1",x:10,y:10,z:"t1",type:"test",wires:[]}, + {id:"t1",type:"tab"} + ]; + flows.init({},storage); + flows.setFlows(originalConfig).then(function() { + credentialsExtract.called.should.be.false; + credentialsClean.called.should.be.true; + storage.hasOwnProperty('conf').should.be.true; + flows.getFlows().should.eql(originalConfig); + done(); + }); + + }); + it('sets the full flow for type load', function(done) { + var originalConfig = [ + {id:"t1-1",x:10,y:10,z:"t1",type:"test",wires:[]}, + {id:"t1",type:"tab"} + ]; + flows.init({},storage); + flows.setFlows(originalConfig,"load").then(function() { + credentialsExtract.called.should.be.false; + credentialsClean.called.should.be.true; + // 'load' type does not trigger a save + storage.hasOwnProperty('conf').should.be.false; + flows.getFlows().should.eql(originalConfig); + done(); + }); + + }); + + it('extracts credentials from the full flow', function(done) { + var originalConfig = [ + {id:"t1-1",x:10,y:10,z:"t1",type:"test",wires:[],credentials:{}}, + {id:"t1",type:"tab"} + ]; + flows.init({},storage); + flows.setFlows(originalConfig).then(function() { + credentialsExtract.called.should.be.true; + credentialsClean.called.should.be.true; + storage.hasOwnProperty('conf').should.be.true; + var cleanedFlows = flows.getFlows(); + cleanedFlows.should.not.eql(originalConfig); + cleanedFlows[0].credentials = {}; + cleanedFlows.should.eql(originalConfig); + done(); + }); + + }); + + }); + + describe('#load', function() { + it('loads the flow config', 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(originalConfig); + } + flows.init({},storage); + flows.load().then(function() { + credentialsExtract.called.should.be.false; + credentialsLoad.called.should.be.true; + credentialsClean.called.should.be.true; + // 'load' type does not trigger a save + storage.hasOwnProperty('conf').should.be.false; + flows.getFlows().should.eql(originalConfig); + done(); + }); + + }); + }); + + describe('#startFlows', function() { + it.skip('starts the loaded config', 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(originalConfig); + } + flows.init({},storage); + flows.load().then(function() { + flows.startFlows(); + // TODO: PICK IT UP FROM HERE + done(); + }); + }); + }); + + describe('#get',function() { + + }); + + describe('#eachNode', function() { + + }); + + describe('#stopFlows', function() { + + }); + describe('#handleError', function() { + + }); + describe('#handleStatus', function() { + + }); +}); + + // 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 new file mode 100644 index 000000000..47f08df6d --- /dev/null +++ b/test/red/nodes/flows/util_spec.js @@ -0,0 +1,594 @@ +/** + * Copyright 2015 IBM Corp. + * + * 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 when = require("when"); +var clone = require("clone"); +var flowUtil = require("../../../../red/nodes/flows/util"); +var typeRegistry = require("../../../../red/nodes/registry"); +var redUtil = require("../../../../red/util"); + +describe('flows/util', function() { + var getType; + + before(function() { + getType = sinon.stub(typeRegistry,"get",function(type) { + return type!=='missing'; + }); + }); + after(function() { + getType.restore(); + }); + + + + describe('#diffNodes',function() { + it('handles a null old node', function() { + flowUtil.diffNodes(null,{}).should.be.true; + }); + it('ignores x/y changes', function() { + flowUtil.diffNodes({x:10,y:10},{x:20,y:10}).should.be.false; + flowUtil.diffNodes({x:10,y:10},{x:10,y:20}).should.be.false; + }); + it('ignores wiring changes', function() { + flowUtil.diffNodes({wires:[]},{wires:[1,2,3]}).should.be.false; + }); + it('spots existing property change - string', function() { + flowUtil.diffNodes({a:"foo"},{a:"bar"}).should.be.true; + }); + it('spots existing property change - number', function() { + flowUtil.diffNodes({a:0},{a:1}).should.be.true; + }); + it('spots existing property change - boolean', function() { + flowUtil.diffNodes({a:true},{a:false}).should.be.true; + }); + it('spots existing property change - truthy', function() { + flowUtil.diffNodes({a:true},{a:1}).should.be.true; + }); + it('spots existing property change - falsey', function() { + flowUtil.diffNodes({a:false},{a:0}).should.be.true; + }); + it('spots existing property change - array', function() { + flowUtil.diffNodes({a:[0,1,2]},{a:[0,2,3]}).should.be.true; + }); + it('spots existing property change - object', function() { + flowUtil.diffNodes({a:{a:[0,1,2]}},{a:{a:[0,2,3]}}).should.be.true; + flowUtil.diffNodes({a:{a:[0,1,2]}},{a:{b:[0,1,2]}}).should.be.true; + }); + it('spots added property', function() { + flowUtil.diffNodes({a:"foo"},{a:"foo",b:"bar"}).should.be.true; + }); + it('spots removed property', function() { + flowUtil.diffNodes({a:"foo",b:"bar"},{a:"foo"}).should.be.true; + }); + + + }); + + describe('#parseConfig',function() { + + it('parses a single-tab flow', function() { + var originalConfig = [ + {id:"t1-1",x:10,y:10,z:"t1",type:"test",wires:[]}, + {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","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 multi-tab flow', function() { + var originalConfig = [ + {id:"t1",type:"tab"}, + {id:"t1-1",x:10,y:10,z:"t1",type:"test",wires:[]}, + {id:"t2",type:"tab"}, + {id:"t2-1",x:10,y:10,z:"t2",type:"test",wires:[]} + ]; + var parsedConfig = flowUtil.parseConfig(originalConfig); + var expectedConfig = {"allNodes":{"t1":{"id":"t1","type":"tab"},"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"test","wires":[]},"t2":{"id":"t2","type":"tab"},"t2-1":{"id":"t2-1","x":10,"y":10,"z":"t2","type":"test","wires":[]}},"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":[]}}},"t2":{"id":"t2","type":"tab","subflows":{},"configs":{},"nodes":{"t2-1":{"id":"t2-1","x":10,"y":10,"z":"t2","type":"test","wires":[]}}}},"missingTypes":[]}; + + redUtil.compareObjects(parsedConfig,expectedConfig).should.be.true; + }); + + it('parses a subflow flow', function() { + var originalConfig = [ + {id:"t1",type:"tab"}, + {id:"t1-1",x:10,y:10,z:"t1",type:"subflow:sf1",wires:[]}, + {id:"sf1",type:"subflow"}, + {id:"sf1-1",x:10,y:10,z:"sf1",type:"test",wires:[]} + ]; + var parsedConfig = flowUtil.parseConfig(originalConfig); + var expectedConfig = {"allNodes":{"t1":{"id":"t1","type":"tab"},"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"subflow:sf1","wires":[]},"sf1":{"id":"sf1","type":"subflow"},"sf1-1":{"id":"sf1-1","x":10,"y":10,"z":"sf1","type":"test","wires":[]}},"subflows":{"sf1":{"id":"sf1","type":"subflow","configs":{},"nodes":{"sf1-1":{"id":"sf1-1","x":10,"y":10,"z":"sf1","type":"test","wires":[]}},"instances":[{"id":"t1-1","x":10,"y":10,"z":"t1","type":"subflow:sf1","wires":[],"subflow":"sf1"}]}},"configs":{},"flows":{"t1":{"id":"t1","type":"tab","subflows":{},"configs":{},"nodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"subflow:sf1","wires":[],"subflow":"sf1"}}}},"missingTypes":[]}; + + redUtil.compareObjects(parsedConfig,expectedConfig).should.be.true; + }); + + it('parses a flow with a missing type', function() { + var originalConfig = [ + {id:"t1",type:"tab"}, + {id:"t1-1",x:10,y:10,z:"t1",type:"sf1",wires:[]}, + {id:"t1-2",x:10,y:10,z:"t1",type:"missing",wires:[]}, + ]; + var parsedConfig = flowUtil.parseConfig(originalConfig); + parsedConfig.missingTypes.should.eql(['missing']); + var expectedConfig = {"allNodes":{"t1":{"id":"t1","type":"tab"},"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"sf1","wires":[]},"t1-2":{"id":"t1-2","x":10,"y":10,"z":"t1","type":"missing","wires":[]}},"subflows":{},"configs":{},"flows":{"t1":{"id":"t1","type":"tab","subflows":{},"configs":{},"nodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"sf1","wires":[]}}}},"missingTypes":["missing"]}; + + redUtil.compareObjects(parsedConfig,expectedConfig).should.be.true; + }); + + + }); + + describe('#diffConfigs', function() { + + it('handles an identical configuration', function() { + var config = [{id:"123",type:"test",foo:"a",wires:[]}]; + + var originalConfig = flowUtil.parseConfig(clone(config)); + var changedConfig = flowUtil.parseConfig(clone(config)); + + originalConfig.missingTypes.should.have.length(0); + + var diffResult = flowUtil.diffConfigs(originalConfig,changedConfig); + + diffResult.added.should.have.length(0); + diffResult.changed.should.have.length(0); + diffResult.removed.should.have.length(0); + diffResult.rewired.should.have.length(0); + diffResult.linked.should.have.length(0); + }); + + it('identifies nodes with changed properties, including downstream linked', function() { + var config = [{id:"1",type:"test",foo:"a",wires:[]},{id:"2",type:"test",bar:"b",wires:[[1]]},{id:"3",type:"test",foo:"a",wires:[]}]; + var newConfig = clone(config); + newConfig[0].foo = "b"; + + var originalConfig = flowUtil.parseConfig(config); + var changedConfig = flowUtil.parseConfig(newConfig); + + originalConfig.missingTypes.should.have.length(0); + + var diffResult = flowUtil.diffConfigs(originalConfig,changedConfig); + diffResult.added.should.have.length(0); + diffResult.changed.should.eql(["1"]); + diffResult.removed.should.have.length(0); + diffResult.rewired.should.have.length(0); + diffResult.linked.should.eql(["2"]); + + }); + it('identifies nodes with changed properties, including upstream linked', function() { + var config = [{id:"1",type:"test",foo:"a",wires:[]},{id:"2",type:"test",bar:"b",wires:[["1"]]},{id:"3",type:"test",foo:"a",wires:[]}]; + var newConfig = clone(config); + newConfig[1].bar = "c"; + + var originalConfig = flowUtil.parseConfig(config); + var changedConfig = flowUtil.parseConfig(newConfig); + + originalConfig.missingTypes.should.have.length(0); + + var diffResult = flowUtil.diffConfigs(originalConfig,changedConfig); + diffResult.added.should.have.length(0); + diffResult.changed.should.eql(["2"]); + diffResult.removed.should.have.length(0); + diffResult.rewired.should.have.length(0); + diffResult.linked.should.eql(["1"]); + }); + + it('identifies nodes with changed credentials, including downstream linked', function() { + var config = [{id:"1",type:"test",wires:[]},{id:"2",type:"test",bar:"b",wires:[["1"]]},{id:"3",type:"test",foo:"a",wires:[]}]; + var newConfig = clone(config); + newConfig[0].credentials = {}; + + var originalConfig = flowUtil.parseConfig(config); + var changedConfig = flowUtil.parseConfig(newConfig); + + originalConfig.missingTypes.should.have.length(0); + + var diffResult = flowUtil.diffConfigs(originalConfig,changedConfig); + diffResult.added.should.have.length(0); + diffResult.changed.should.eql(["1"]); + diffResult.removed.should.have.length(0); + diffResult.rewired.should.have.length(0); + diffResult.linked.should.eql(["2"]); + }); + + it('identifies nodes with changed wiring', function() { + var config = [{id:"1",type:"test",foo:"a",wires:[]},{id:"2",type:"test",bar:"b",wires:[["1"]]},{id:"3",type:"test",foo:"a",wires:[]}]; + var newConfig = clone(config); + newConfig[1].wires[0][0] = "3"; + + var originalConfig = flowUtil.parseConfig(config); + var changedConfig = flowUtil.parseConfig(newConfig); + + originalConfig.missingTypes.should.have.length(0); + + var diffResult = flowUtil.diffConfigs(originalConfig,changedConfig); + diffResult.added.should.have.length(0); + diffResult.changed.should.have.length(0); + diffResult.removed.should.have.length(0); + diffResult.rewired.should.eql(["2"]); + diffResult.linked.sort().should.eql(["1","3"]); + }); + + it('identifies nodes with changed wiring - second connection added', function() { + var config = [{id:"1",type:"test",foo:"a",wires:[]},{id:"2",type:"test",bar:"b",wires:[["1"]]},{id:"3",type:"test",foo:"a",wires:[]}]; + var newConfig = clone(config); + newConfig[1].wires[0].push("1"); + + var originalConfig = flowUtil.parseConfig(config); + var changedConfig = flowUtil.parseConfig(newConfig); + + originalConfig.missingTypes.should.have.length(0); + + var diffResult = flowUtil.diffConfigs(originalConfig,changedConfig); + diffResult.added.should.have.length(0); + diffResult.changed.should.have.length(0); + diffResult.removed.should.have.length(0); + diffResult.rewired.should.eql(["2"]); + diffResult.linked.sort().should.eql(["1"]); + }); + + it('identifies nodes with changed wiring - node connected', function() { + var config = [{id:"1",type:"test",foo:"a",wires:[["2"]]},{id:"2",type:"test",bar:"b",wires:[[]]},{id:"3",type:"test",foo:"a",wires:[]}]; + var newConfig = clone(config); + newConfig[1].wires.push("3"); + + var originalConfig = flowUtil.parseConfig(config); + var changedConfig = flowUtil.parseConfig(newConfig); + + originalConfig.missingTypes.should.have.length(0); + + var diffResult = flowUtil.diffConfigs(originalConfig,changedConfig); + diffResult.added.should.have.length(0); + diffResult.changed.should.have.length(0); + diffResult.removed.should.have.length(0); + diffResult.rewired.should.eql(["2"]); + diffResult.linked.sort().should.eql(["1","3"]); + }); + + it('identifies new nodes', function() { + var config = [{id:"1",type:"test",foo:"a",wires:[]},{id:"3",type:"test",foo:"a",wires:[]}]; + var newConfig = clone(config); + newConfig.push({id:"2",type:"test",bar:"b",wires:[["1"]]}); + + var originalConfig = flowUtil.parseConfig(config); + var changedConfig = flowUtil.parseConfig(newConfig); + + originalConfig.missingTypes.should.have.length(0); + + var diffResult = flowUtil.diffConfigs(originalConfig,changedConfig); + diffResult.added.should.eql(["2"]); + diffResult.changed.should.have.length(0); + diffResult.removed.should.have.length(0); + diffResult.rewired.should.have.length(0); + diffResult.linked.sort().should.eql(["1"]); + }); + + it('identifies deleted nodes', function() { + var config = [{id:"1",type:"test",foo:"a",wires:[["2"]]},{id:"2",type:"test",bar:"b",wires:[["3"]]},{id:"3",type:"test",foo:"a",wires:[]}]; + var newConfig = clone(config); + newConfig.splice(1,1); + newConfig[0].wires = []; + + var originalConfig = flowUtil.parseConfig(config); + var changedConfig = flowUtil.parseConfig(newConfig); + + originalConfig.missingTypes.should.have.length(0); + + var diffResult = flowUtil.diffConfigs(originalConfig,changedConfig); + diffResult.added.should.have.length(0); + diffResult.changed.should.have.length(0); + diffResult.removed.should.eql(["2"]); + diffResult.rewired.should.eql(["1"]); + diffResult.linked.sort().should.eql(["3"]); + }); + + it('identifies config nodes changes', function() { + var config = [ + {id:"1",type:"test",foo:"configNode",wires:[["2"]]}, + {id:"2",type:"test",bar:"b",wires:[["3"]]}, + {id:"3",type:"test",foo:"a",wires:[]}, + {id:"configNode",type:"testConfig"} + ]; + var newConfig = clone(config); + newConfig[3].foo = "bar"; + + var originalConfig = flowUtil.parseConfig(config); + var changedConfig = flowUtil.parseConfig(newConfig); + + originalConfig.missingTypes.should.have.length(0); + + var diffResult = flowUtil.diffConfigs(originalConfig,changedConfig); + diffResult.added.should.have.length(0); + diffResult.changed.sort().should.eql(["1","configNode"]); + diffResult.removed.should.have.length(0); + diffResult.rewired.should.have.length(0); + diffResult.linked.sort().should.eql(["2","3"]); + + }); + + it('marks a parent subflow as changed for an internal property change', function() { + var config = [ + {id:"1",type:"test",wires:[["2"]]}, + {id:"2",type:"subflow:sf1",wires:[["3"]]}, + {id:"3",type:"test",wires:[]}, + {id:"sf1",type:"subflow"}, + {id:"sf1-1",z:"sf1",type:"test",foo:"a",wires:[]}, + {id:"4",type:"subflow:sf1",wires:[]} + ]; + + var newConfig = clone(config); + newConfig[4].foo = "b"; + + var originalConfig = flowUtil.parseConfig(config); + var changedConfig = flowUtil.parseConfig(newConfig); + + originalConfig.missingTypes.should.have.length(0); + + var diffResult = flowUtil.diffConfigs(originalConfig,changedConfig); + diffResult.added.should.have.length(0); + diffResult.changed.sort().should.eql(['2', '4', 'sf1']); + diffResult.removed.should.have.length(0); + diffResult.rewired.should.have.length(0); + diffResult.linked.sort().should.eql(["1","3"]); + + + }); + + it('marks a parent subflow as changed for an internal wiring change', function() { + var config = [ + {id:"1",type:"test",wires:[["2"]]}, + {id:"2",type:"subflow:sf1",wires:[["3"]]}, + {id:"3",type:"test",wires:[]}, + {id:"sf1",type:"subflow"}, + {id:"sf1-1",z:"sf1",type:"test",wires:[]}, + {id:"sf1-2",z:"sf1",type:"test",wires:[]} + ]; + + var newConfig = clone(config); + newConfig[4].wires = [["sf1-2"]]; + + var originalConfig = flowUtil.parseConfig(config); + var changedConfig = flowUtil.parseConfig(newConfig); + + originalConfig.missingTypes.should.have.length(0); + + var diffResult = flowUtil.diffConfigs(originalConfig,changedConfig); + diffResult.added.should.have.length(0); + diffResult.changed.sort().should.eql(['2', 'sf1']); + diffResult.removed.should.have.length(0); + diffResult.rewired.should.have.length(0); + diffResult.linked.sort().should.eql(["1","3"]); + }); + + it('marks a parent subflow as changed for an internal node add', function() { + var config = [ + {id:"1",type:"test",wires:[["2"]]}, + {id:"2",type:"subflow:sf1",wires:[["3"]]}, + {id:"3",type:"test",wires:[]}, + {id:"sf1",type:"subflow"}, + {id:"sf1-1",z:"sf1",type:"test",wires:[]}, + {id:"sf1-2",z:"sf1",type:"test",wires:[]} + ]; + + var newConfig = clone(config); + newConfig.push({id:"sf1-3",z:"sf1",type:"test",wires:[]}); + + var originalConfig = flowUtil.parseConfig(config); + var changedConfig = flowUtil.parseConfig(newConfig); + + originalConfig.missingTypes.should.have.length(0); + + var diffResult = flowUtil.diffConfigs(originalConfig,changedConfig); + diffResult.added.should.have.length(0); + diffResult.changed.sort().should.eql(['2', 'sf1']); + diffResult.removed.should.have.length(0); + diffResult.rewired.should.have.length(0); + diffResult.linked.sort().should.eql(["1","3"]); + + }); + + it('marks a parent subflow as changed for an internal node delete', function() { + var config = [ + {id:"1",type:"test",wires:[["2"]]}, + {id:"2",type:"subflow:sf1",wires:[["3"]]}, + {id:"3",type:"test",wires:[]}, + {id:"sf1",type:"subflow"}, + {id:"sf1-1",z:"sf1",type:"test",wires:[]}, + {id:"sf1-2",z:"sf1",type:"test",wires:[]} + ]; + + var newConfig = clone(config); + newConfig.splice(5,1); + + var originalConfig = flowUtil.parseConfig(config); + var changedConfig = flowUtil.parseConfig(newConfig); + + originalConfig.missingTypes.should.have.length(0); + + var diffResult = flowUtil.diffConfigs(originalConfig,changedConfig); + diffResult.added.should.have.length(0); + diffResult.changed.sort().should.eql(['2', 'sf1']); + diffResult.removed.should.have.length(0); + diffResult.rewired.should.have.length(0); + diffResult.linked.sort().should.eql(["1","3"]); + + }); + + it('marks a parent subflow as changed for an internal subflow wiring change - input removed', function() { + var config = [ + {id:"1",type:"test",wires:[["2"]]}, + {id:"2",type:"subflow:sf1",wires:[["3"]]}, + {id:"3",type:"test",wires:[]}, + {id:"sf1",type:"subflow","in": [{"wires": [{"id": "sf1-1"}]}],"out": [{"wires": [{"id": "sf1-2","port": 0}]}]}, + {id:"sf1-1",z:"sf1",type:"test",wires:[]}, + {id:"sf1-2",z:"sf1",type:"test",wires:[]} + ]; + + var newConfig = clone(config); + newConfig[3].in[0].wires = []; + + var originalConfig = flowUtil.parseConfig(config); + var changedConfig = flowUtil.parseConfig(newConfig); + + originalConfig.missingTypes.should.have.length(0); + + var diffResult = flowUtil.diffConfigs(originalConfig,changedConfig); + diffResult.added.should.have.length(0); + diffResult.changed.sort().should.eql(['2', 'sf1']); + diffResult.removed.should.have.length(0); + diffResult.rewired.should.have.length(0); + diffResult.linked.sort().should.eql(["1","3"]); + }); + + it('marks a parent subflow as changed for an internal subflow wiring change - input added', function() { + var config = [ + {id:"1",type:"test",wires:[["2"]]}, + {id:"2",type:"subflow:sf1",wires:[["3"]]}, + {id:"3",type:"test",wires:[]}, + {id:"sf1",type:"subflow","in": [{"wires": [{"id": "sf1-1"}]}],"out": [{"wires": [{"id": "sf1-2","port": 0}]}]}, + {id:"sf1-1",z:"sf1",type:"test",wires:[]}, + {id:"sf1-2",z:"sf1",type:"test",wires:[]} + ]; + + var newConfig = clone(config); + newConfig[3].in[0].wires.push({"id":"sf1-2"}); + + var originalConfig = flowUtil.parseConfig(config); + var changedConfig = flowUtil.parseConfig(newConfig); + + originalConfig.missingTypes.should.have.length(0); + + var diffResult = flowUtil.diffConfigs(originalConfig,changedConfig); + diffResult.added.should.have.length(0); + diffResult.changed.sort().should.eql(['2', 'sf1']); + diffResult.removed.should.have.length(0); + diffResult.rewired.should.have.length(0); + diffResult.linked.sort().should.eql(["1","3"]); + }); + + it('marks a parent subflow as changed for an internal subflow wiring change - output added', function() { + var config = [ + {id:"1",type:"test",wires:[["2"]]}, + {id:"2",type:"subflow:sf1",wires:[["3"]]}, + {id:"3",type:"test",wires:[]}, + {id:"sf1",type:"subflow","in": [{"wires": [{"id": "sf1-1"}]}],"out": [{"wires": [{"id": "sf1-2","port": 0}]}]}, + {id:"sf1-1",z:"sf1",type:"test",wires:[]}, + {id:"sf1-2",z:"sf1",type:"test",wires:[]} + ]; + + var newConfig = clone(config); + newConfig[3].out[0].wires.push({"id":"sf1-2","port":0}); + + var originalConfig = flowUtil.parseConfig(config); + var changedConfig = flowUtil.parseConfig(newConfig); + + originalConfig.missingTypes.should.have.length(0); + + var diffResult = flowUtil.diffConfigs(originalConfig,changedConfig); + diffResult.added.should.have.length(0); + diffResult.changed.sort().should.eql(['2', 'sf1']); + diffResult.removed.should.have.length(0); + diffResult.rewired.should.have.length(0); + diffResult.linked.sort().should.eql(["1","3"]); + }); + + it('marks a parent subflow as changed for an internal subflow wiring change - output removed', function() { + var config = [ + {id:"1",type:"test",wires:[["2"]]}, + {id:"2",type:"subflow:sf1",wires:[["3"]]}, + {id:"3",type:"test",wires:[]}, + {id:"sf1",type:"subflow","in": [{"wires": [{"id": "sf1-1"}]}],"out": [{"wires": [{"id": "sf1-2","port": 0}]}]}, + {id:"sf1-1",z:"sf1",type:"test",wires:[]}, + {id:"sf1-2",z:"sf1",type:"test",wires:[]} + ]; + + var newConfig = clone(config); + newConfig[3].out[0].wires = []; + + var originalConfig = flowUtil.parseConfig(config); + var changedConfig = flowUtil.parseConfig(newConfig); + + originalConfig.missingTypes.should.have.length(0); + + var diffResult = flowUtil.diffConfigs(originalConfig,changedConfig); + diffResult.added.should.have.length(0); + diffResult.changed.sort().should.eql(['2', 'sf1']); + diffResult.removed.should.have.length(0); + diffResult.rewired.should.have.length(0); + diffResult.linked.sort().should.eql(["1","3"]); + }); + + it('marks a parent subflow as changed for a global config node change', function() { + var config = [ + {id:"1",type:"test",wires:[["2"]]}, + {id:"2",type:"subflow:sf1",wires:[["3"]]}, + {id:"3",type:"test",wires:[]}, + {id:"sf1",type:"subflow"}, + {id:"sf1-1",z:"sf1",prop:"configNode",type:"test",wires:[]}, + {id:"sf1-2",z:"sf1",type:"test",wires:[]}, + {id:"configNode",a:"foo",type:"test",wires:[]} + ]; + + var newConfig = clone(config); + newConfig[6].a = "bar"; + + var originalConfig = flowUtil.parseConfig(config); + var changedConfig = flowUtil.parseConfig(newConfig); + + originalConfig.missingTypes.should.have.length(0); + + var diffResult = flowUtil.diffConfigs(originalConfig,changedConfig); + diffResult.added.should.have.length(0); + diffResult.changed.sort().should.eql(['2', "configNode", 'sf1']); + diffResult.removed.should.have.length(0); + diffResult.rewired.should.have.length(0); + diffResult.linked.sort().should.eql(["1","3"]); + }); + + it('marks a parent subflow as changed for an internal subflow instance change', function() { + var config = [ + {id:"1",type:"test",wires:[["2"]]}, + {id:"2",type:"subflow:sf1",wires:[["3"]]}, + {id:"3",type:"test",wires:[]}, + {id:"sf1",type:"subflow"}, + {id:"sf2",type:"subflow"}, + {id:"sf1-1",z:"sf1",type:"test",wires:[]}, + {id:"sf1-2",z:"sf1",type:"subflow:sf2",wires:[]}, + {id:"sf2-1",z:"sf2",type:"test",wires:[]}, + {id:"sf2-2",z:"sf2",type:"test",wires:[]}, + ]; + + var newConfig = clone(config); + newConfig[8].a = "bar"; + + var originalConfig = flowUtil.parseConfig(config); + var changedConfig = flowUtil.parseConfig(newConfig); + + originalConfig.missingTypes.should.have.length(0); + + var diffResult = flowUtil.diffConfigs(originalConfig,changedConfig); + diffResult.added.should.have.length(0); + diffResult.changed.sort().should.eql(['2', 'sf1', 'sf2']); + diffResult.removed.should.have.length(0); + diffResult.rewired.should.have.length(0); + diffResult.linked.sort().should.eql(["1","3"]); + }); + }); +}); diff --git a/test/red/nodes/flows_spec.js b/test/red/nodes/flows_spec.js deleted file mode 100644 index b986f92f1..000000000 --- a/test/red/nodes/flows_spec.js +++ /dev/null @@ -1,230 +0,0 @@ -/** - * Copyright 2014 IBM Corp. - * - * 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 when = require("when"); -var clone = require("clone"); -var flows = require("../../../red/nodes/flows"); -var RedNode = require("../../../red/nodes/Node"); -var RED = require("../../../red/nodes"); -var events = require("../../../red/events"); -var credentials = require("../../../red/nodes/credentials"); -var typeRegistry = require("../../../red/nodes/registry"); -var Flow = require("../../../red/nodes/Flow"); - - -var settings = { - available: function() { return false; } -} - -function loadFlows(testFlows, cb) { - var storage = { - getFlows: function() { - return when.resolve(testFlows); - }, - getCredentials: function() { - return when.resolve({}); - } - }; - RED.init(settings, storage); - flows.load().then(function() { - should.deepEqual(testFlows, flows.getFlows()); - cb(); - }); -} - -describe('flows', function() { - - 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('should load and start an empty tab flow',function(done) { - loadFlows([{"type":"tab","id":"tab1","label":"Sheet 1"}], function() {}); - events.once('nodes-started', function() { done(); }); - }); - - it('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('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('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('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('#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/deprecated_spec.js b/test/red/nodes/registry/deprecated_spec.js similarity index 92% rename from test/red/nodes/deprecated_spec.js rename to test/red/nodes/registry/deprecated_spec.js index f534b2f2a..033260d9c 100644 --- a/test/red/nodes/deprecated_spec.js +++ b/test/red/nodes/registry/deprecated_spec.js @@ -13,10 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. **/ - + var should = require("should"); -var deprecated = require("../../../red/nodes/deprecated.js"); +var deprecated = require("../../../../red/nodes/registry/deprecated.js"); describe('deprecated', function() { it('should return info on a node',function() { diff --git a/test/red/server_spec.js b/test/red/server_spec.js index 60ab652af..9607747c4 100644 --- a/test/red/server_spec.js +++ b/test/red/server_spec.js @@ -30,11 +30,11 @@ var log = require("../../red/log"); describe("red/server", function() { var commsMessages = []; var commsPublish; - + beforeEach(function() { commsMessages = []; }); - + before(function() { commsPublish = sinon.stub(comms,"publish", function(topic,msg,retained) { commsMessages.push({topic:topic,msg:msg,retained:retained}); @@ -43,22 +43,22 @@ describe("red/server", function() { after(function() { commsPublish.restore(); }); - + it("initialises components", function() { var commsInit = sinon.stub(comms,"init",function() {}); var dummyServer = {}; server.init(dummyServer,{testSettings: true, httpAdminRoot:"/", load:function() { return when.resolve();}}); - + commsInit.called.should.be.true; - + should.exist(server.app); should.exist(server.nodeApp); - + server.server.should.equal(dummyServer); - + commsInit.restore(); }); - + describe("start",function() { var commsInit; var storageInit; @@ -73,8 +73,9 @@ describe("red/server", function() { var redNodesCleanModuleList; var redNodesGetNodeList; var redNodesLoadFlows; + var redNodesStartFlows; var commsStart; - + beforeEach(function() { commsInit = sinon.stub(comms,"init",function() {}); storageInit = sinon.stub(storage,"init",function(settings) {return when.resolve();}); @@ -86,7 +87,8 @@ describe("red/server", function() { redNodesInit = sinon.stub(redNodes,"init", function() {}); redNodesLoad = sinon.stub(redNodes,"load", function() {return when.resolve()}); redNodesCleanModuleList = sinon.stub(redNodes,"cleanModuleList",function(){}); - redNodesLoadFlows = sinon.stub(redNodes,"loadFlows",function() {}); + redNodesLoadFlows = sinon.stub(redNodes,"loadFlows",function() {return when.resolve()}); + redNodesStartFlows = sinon.stub(redNodes,"startFlows",function() {}); commsStart = sinon.stub(comms,"start",function(){}); }); afterEach(function() { @@ -99,9 +101,10 @@ describe("red/server", function() { logLog.restore(); redNodesInit.restore(); redNodesLoad.restore(); - redNodesGetNodeList.restore(); + redNodesGetNodeList.restore(); redNodesCleanModuleList.restore(); redNodesLoadFlows.restore(); + redNodesStartFlows.restore(); commsStart.restore(); }); it("reports errored/missing modules",function(done) { @@ -120,7 +123,7 @@ describe("red/server", function() { redNodesLoad.calledOnce.should.be.true; commsStart.calledOnce.should.be.true; redNodesLoadFlows.calledOnce.should.be.true; - + logWarn.calledWithMatch("Failed to register 1 node type"); logWarn.calledWithMatch("Missing node modules"); logWarn.calledWithMatch(" - module: typeA, typeB"); @@ -168,7 +171,7 @@ describe("red/server", function() { }); server.init({},{testSettings: true, verbose:true, httpAdminRoot:"/", load:function() { return when.resolve();}}); server.start().then(function() { - + try { apiInit.calledOnce.should.be.true; logWarn.neverCalledWithMatch("Failed to register 1 node type"); @@ -179,7 +182,7 @@ describe("red/server", function() { } }); }); - + it("reports runtime metrics",function(done) { var commsStop = sinon.stub(comms,"stop",function() {} ); var stopFlows = sinon.stub(redNodes,"stopFlows",function() {} ); @@ -208,8 +211,8 @@ describe("red/server", function() { } },500); }); - }); - + }); + it("doesn't init api if httpAdminRoot set to false",function(done) { redNodesGetNodeList = sinon.stub(redNodes,"getNodeList", function() {return []}); server.init({},{testSettings: true, httpAdminRoot:false, load:function() { return when.resolve();}}); @@ -225,20 +228,20 @@ describe("red/server", function() { }); }); }); - + it("stops components", function() { var commsStop = sinon.stub(comms,"stop",function() {} ); var stopFlows = sinon.stub(redNodes,"stopFlows",function() {} ); - + server.stop(); - + commsStop.called.should.be.true; stopFlows.called.should.be.true; - + commsStop.restore(); stopFlows.restore(); }); - + it("reports added modules", function() { var nodes = {nodes:[ {types:["a"]}, @@ -246,13 +249,13 @@ describe("red/server", function() { {types:["c"],err:"error"} ]}; var result = server.reportAddedModules(nodes); - + result.should.equal(nodes); commsMessages.should.have.length(1); commsMessages[0].topic.should.equal("node/added"); commsMessages[0].msg.should.eql(nodes.nodes); }); - + it("reports removed modules", function() { var nodes = [ {types:["a"]}, @@ -260,13 +263,13 @@ describe("red/server", function() { {types:["c"],err:"error"} ]; var result = server.reportRemovedModules(nodes); - + result.should.equal(nodes); commsMessages.should.have.length(1); commsMessages[0].topic.should.equal("node/removed"); commsMessages[0].msg.should.eql(nodes); }); - + describe("installs module", function() { it("rejects invalid module names", function(done) { var promises = []; @@ -278,12 +281,12 @@ describe("red/server", function() { done(); }); }); - + it("rejects when npm returns a 404", function(done) { var exec = sinon.stub(child_process,"exec",function(cmd,opt,cb) { cb(new Error(),""," 404 this_wont_exist"); }); - + server.installModule("this_wont_exist").otherwise(function(err) { err.code.should.be.eql(404); done(); @@ -295,7 +298,7 @@ describe("red/server", function() { var exec = sinon.stub(child_process,"exec",function(cmd,opt,cb) { cb(new Error("test_error"),"",""); }); - + server.installModule("this_wont_exist").then(function() { done(new Error("Unexpected success")); }).otherwise(function(err) { @@ -312,7 +315,7 @@ describe("red/server", function() { var addModule = sinon.stub(redNodes,"addModule",function(md) { return when.resolve(nodeInfo); }); - + server.installModule("this_wont_exist").then(function(info) { info.should.eql(nodeInfo); commsMessages.should.have.length(1); @@ -347,7 +350,7 @@ describe("red/server", function() { var exec = sinon.stub(child_process,"exec",function(cmd,opt,cb) { cb(new Error("test_error"),"",""); }); - + server.uninstallModule("this_wont_exist").then(function() { done(new Error("Unexpected success")); }).otherwise(function(err) { @@ -366,7 +369,7 @@ describe("red/server", function() { cb(null,"",""); }); var exists = sinon.stub(fs,"existsSync", function(fn) { return true; }); - + server.uninstallModule("this_wont_exist").then(function(info) { info.should.eql(nodeInfo); commsMessages.should.have.length(1); @@ -382,5 +385,5 @@ describe("red/server", function() { }); }); }); - + }); From 5a176a037c121f2cef8cd113d4681a5ada6a67c1 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Fri, 30 Oct 2015 12:48:09 +0000 Subject: [PATCH 2/4] Update test helper for refactored flow engine --- test/nodes/helper.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/nodes/helper.js b/test/nodes/helper.js index 87c155e4d..3d3f72638 100644 --- a/test/nodes/helper.js +++ b/test/nodes/helper.js @@ -110,6 +110,7 @@ module.exports = { testNode(red); } flows.load().then(function() { + flows.startFlows(); should.deepEqual(testFlows, flows.getFlows()); cb(); }); From d1940a023a64c317d873e3294d84a2698d5ca743 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Mon, 2 Nov 2015 15:38:16 +0000 Subject: [PATCH 3/4] Complete test coverage on flow engine refactor --- red/nodes/Node.js | 1 + red/nodes/credentials.js | 18 +- red/nodes/flows/Flow.js | 6 +- red/nodes/flows/index.js | 62 +- test/red/nodes/flows/Flow_spec.js | 1593 +++++++++++++++------------- test/red/nodes/flows/index_spec.js | 240 ++++- test/red/nodes/index_spec.js | 7 + 7 files changed, 1099 insertions(+), 828 deletions(-) diff --git a/red/nodes/Node.js b/red/nodes/Node.js index 9d1120704..ee988ed1f 100644 --- a/red/nodes/Node.js +++ b/red/nodes/Node.js @@ -39,6 +39,7 @@ function Node(n) { util.inherits(Node, EventEmitter); Node.prototype.updateWires = function(wires) { + //console.log("UPDATE",this.id); this.wires = wires || []; delete this._wire; diff --git a/red/nodes/credentials.js b/red/nodes/credentials.js index d7e80a2d3..603bbf4ae 100644 --- a/red/nodes/credentials.js +++ b/red/nodes/credentials.js @@ -68,7 +68,7 @@ module.exports = { storage = _storage; redApp = _app; }, - + /** * Loads the credentials from storage. */ @@ -79,7 +79,7 @@ module.exports = { log.warn(log._("nodes.credentials.error",{message: err})); }); }, - + /** * Adds a set of credentials for the given node id. * @param id the node id for the credentials @@ -118,7 +118,7 @@ module.exports = { clean: function (config) { var existingIds = {}; config.forEach(function(n) { - existingIds[n.id] = true; + existingIds[n.id] = true; }); var deletedCredentials = false; for (var c in credentialCache) { @@ -135,7 +135,7 @@ module.exports = { return when.resolve(); } }, - + /** * Registers a node credential definition. * @param type the node type @@ -146,14 +146,14 @@ module.exports = { credentialsDef[dashedType] = definition; registerEndpoint(dashedType); }, - + /** * Extracts and stores any credential updates in the provided node. * The provided node may have a .credentials property that contains * new credentials for the node. * This function loops through the credentials in the definition for * the node-type and applies any of the updates provided in the node. - * + * * This function does not save the credentials to disk as it is expected * to be called multiple times when a new flow is deployed. * @@ -171,7 +171,7 @@ module.exports = { log.warn(log._("nodes.credentials.not-registered",{type:nodeType})); return; } - + for (var cred in definition) { if (definition.hasOwnProperty(cred)) { if (newCreds[cred] === undefined) { @@ -191,7 +191,7 @@ module.exports = { delete node.credentials; } }, - + /** * Saves the credentials to storage * @return a promise for the saving of credentials to storage @@ -199,7 +199,7 @@ module.exports = { save: function () { return storage.saveCredentials(credentialCache); }, - + /** * Gets the credential definition for the given node type * @param type the node type diff --git a/red/nodes/flows/Flow.js b/red/nodes/flows/Flow.js index 397537e19..562f1a9be 100644 --- a/red/nodes/flows/Flow.js +++ b/red/nodes/flows/Flow.js @@ -212,6 +212,7 @@ function Flow(global,flow) { source: { id: node.id, type: node.type, + name: node.name, count: count } }; @@ -258,11 +259,12 @@ function mapEnvVarProperties(obj,prop) { } function createNode(type,config) { - //console.log("CREATE",type,config.id); + // console.log("CREATE",type,config.id); var nn = null; var nt = typeRegistry.get(type); if (nt) { var conf = clone(config); + delete conf.credentials; for (var p in conf) { if (conf.hasOwnProperty(p)) { mapEnvVarProperties(conf,p); @@ -371,9 +373,7 @@ function createSubflow(sf,sfn,subflows,globalSubflows,activeNodes) { if (sf.out) { var node,wires,i,j; // Restore the original wiring to the internal nodes - subflowInstance.wires = clone(subflowInstance._originalWires); - for (i=0;i 0) { - var i = activeFlowConfig.missingTypes.indexOf(type); - if (i != -1) { - log.info(log._("nodes.flows.registered-missing", {type:type})); - activeFlowConfig.missingTypes.splice(i,1); - if (activeFlowConfig.missingTypes.length === 0 && started) { - start(); + if (!typeEventRegistered) { + events.on('type-registered',function(type) { + if (activeFlowConfig && activeFlowConfig.missingTypes.length > 0) { + var i = activeFlowConfig.missingTypes.indexOf(type); + if (i != -1) { + log.info(log._("nodes.flows.registered-missing", {type:type})); + activeFlowConfig.missingTypes.splice(i,1); + if (activeFlowConfig.missingTypes.length === 0 && started) { + start(); + } } } - } - }); + }); + typeEventRegistered = true; + } } function load() { return storage.getFlows().then(function(flows) { @@ -67,21 +75,25 @@ function load() { }); } -function setConfig(config,type) { +function setConfig(_config,type) { + var config = clone(_config); type = type||"full"; var credentialsChanged = false; var credentialSavePromise = null; var configSavePromise = null; - var cleanConfig = clone(config); - cleanConfig.forEach(function(node) { + var diff; + var newFlowConfig = flowUtil.parseConfig(clone(config)); + if (type !== 'full' && type !== 'load') { + diff = flowUtil.diffConfigs(activeFlowConfig,newFlowConfig); + } + config.forEach(function(node) { if (node.credentials) { credentials.extract(node); credentialsChanged = true; } }); - if (credentialsChanged) { credentialSavePromise = credentials.save(); } else { @@ -92,27 +104,19 @@ function setConfig(config,type) { type = 'full'; } else { configSavePromise = credentialSavePromise.then(function() { - return storage.saveFlows(cleanConfig); + return storage.saveFlows(config); }); } return configSavePromise .then(function() { - var diff; - activeConfig = cleanConfig; - if (type === 'full') { - activeFlowConfig = flowUtil.parseConfig(clone(config)); - } else { - var newConfig = flowUtil.parseConfig(clone(config)); - diff = flowUtil.diffConfigs(activeFlowConfig,newConfig); - activeFlowConfig = newConfig; - } - credentials.clean(activeConfig).then(function() { + activeConfig = config; + activeFlowConfig = newFlowConfig; + return credentials.clean(activeConfig).then(function() { if (started) { return stop(type,diff).then(function() { start(type,diff); }).otherwise(function(err) { - console.log(err); }) } }); @@ -136,9 +140,9 @@ function getNode(id) { } function eachNode(cb) { - for (var id in activeFlowConfig.nodes) { - if (activeFlowConfig.nodes.hasOwnProperty(id)) { - cb(activeFlowConfig.nodes[id]); + for (var id in activeFlowConfig.allNodes) { + if (activeFlowConfig.allNodes.hasOwnProperty(id)) { + cb(activeFlowConfig.allNodes[id]); } } } diff --git a/test/red/nodes/flows/Flow_spec.js b/test/red/nodes/flows/Flow_spec.js index f23240550..21a007011 100644 --- a/test/red/nodes/flows/Flow_spec.js +++ b/test/red/nodes/flows/Flow_spec.js @@ -20,246 +20,133 @@ var sinon = require('sinon'); var clone = require('clone'); var util = require("util"); -// var Flow = require("../../../../red/nodes/Flow"); -// var flows = require("../../../../red/nodes/flows"); -// var Node = require("../../../../red/nodes/Node"); -// -// var typeRegistry = require("../../../red/nodes/registry"); -// var credentials = require("../../../red/nodes/credentials"); -return; +var flowUtils = require("../../../../red/nodes/flows/util"); +var Flow = require("../../../../red/nodes/flows/Flow"); +var flows = require("../../../../red/nodes/flows"); +var Node = require("../../../../red/nodes/Node"); +var typeRegistry = require("../../../../red/nodes/registry"); -describe.skip('Flow', function() { - describe('#constructor',function() { - it('called with an empty flow',function() { - var config = []; - var flow = new Flow(config); - config.should.eql(flow.getFlow()); - var nodeCount = 0; - flow.eachNode(function(node) { - nodeCount++; - }); +describe('Flow', function() { + var getType; + var getNode; - nodeCount.should.equal(0); - }); - - it('called with a non-empty flow with no missing types', function() { - var getType = sinon.stub(typeRegistry,"get",function(type) { - // For this test, don't care what the actual type is, just - // that this returns a non-false result - return {}; - }); - try { - var config = [{id:"123",type:"test"}]; - var flow = new Flow(config); - config.should.eql(flow.getFlow()); - - flow.getMissingTypes().should.have.length(0); - } finally { - getType.restore(); - } - }); - - it('identifies missing types in a flow', function() { - var getType = sinon.stub(typeRegistry,"get",function(type) { - if (type == "test") { - return {}; - } else { - return null; - } - }); - try { - var config = [{id:"123",type:"test"},{id:"456",type:"test1"},{id:"789",type:"test2"}]; - var flow = new Flow(config); - config.should.eql(flow.getFlow()); - - flow.getMissingTypes().should.eql(["test1","test2"]); - } finally { - getType.restore(); - } - }); - - it('extracts node credentials', function() { - var getType = sinon.stub(typeRegistry,"get",function(type) { - // For this test, don't care what the actual type is, just - // that this returns a non-false result - return {}; - }); - - try { - var config = [{id:"123",type:"test",credentials:{a:1,b:2}}]; - var resultingConfig = clone(config); - delete resultingConfig[0].credentials; - var flow = new Flow(config); - flow.getFlow().should.eql(resultingConfig); - flow.getMissingTypes().should.have.length(0); - } finally { - getType.restore(); - } - }); + var stoppedNodes = {}; + var currentNodes = {}; + var rewiredNodes = {}; + var createCount = 0; + beforeEach(function() { + currentNodes = {}; + stoppedNodes = {}; + rewiredNodes = {}; + createCount = 0; }); - describe('missing types',function() { - it('prevents a flow with missing types from starting', function() { - var getType = sinon.stub(typeRegistry,"get",function(type) { - if (type == "test") { - return {}; - } else { - return null; - } - }); - try { - var config = [{id:"123",type:"test"},{id:"456",type:"test1"},{id:"789",type:"test2"}]; - var flow = new Flow(config); - flow.getMissingTypes().should.have.length(2); - - /*jshint immed: false */ - (function() { - flow.start(); - }).should.throw(); - } finally { - getType.restore(); - } + var TestNode = function(n) { + Node.call(this,n); + 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) { + node.handled++; + node.send(msg); }); - - it('removes missing types as they are registered', function() { - var getType = sinon.stub(typeRegistry,"get",function(type) { - if (type == "test") { - return {}; - } else { - return null; - } - }); - var flowStart; - try { - var config = [{id:"123",type:"test"},{id:"456",type:"test1"},{id:"789",type:"test2"}]; - var flow = new Flow(config); - - flowStart = sinon.stub(flow,"start",function() {this.started = true;}); - config.should.eql(flow.getFlow()); - - flow.getMissingTypes().should.eql(["test1","test2"]); - - var resp = flow.typeRegistered("a-random-node"); - resp.should.be.false; - - resp = flow.typeRegistered("test1"); - resp.should.be.true; - flow.getMissingTypes().should.eql(["test2"]); - flowStart.called.should.be.false; - - resp = flow.typeRegistered("test2"); - resp.should.be.true; - flow.getMissingTypes().should.eql([]); - flowStart.called.should.be.false; - } finally { - flowStart.restore(); - getType.restore(); - } + 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); - it('starts flows once all missing types are registered', function() { - var getType = sinon.stub(typeRegistry,"get",function(type) { - if (type == "test") { - return {}; - } else { - return null; - } - }); - var flowStart; - try { - var config = [{id:"123",type:"test"},{id:"456",type:"test1"},{id:"789",type:"test2"}]; - var flow = new Flow(config); - - // First call to .start throws err due to missing types - /*jshint immed: false */ - (function() { - flow.start(); - }).should.throw(); - - // Stub .start so when missing types are registered, we don't actually try starting them - flowStart = sinon.stub(flow,"start",function() {}); - config.should.eql(flow.getFlow()); - - flow.getMissingTypes().should.eql(["test1","test2"]); - - flow.typeRegistered("test1"); - flow.typeRegistered("test2"); - flow.getMissingTypes().should.have.length(0); - flowStart.called.should.be.true; - } finally { - flowStart.restore(); - getType.restore(); - } + 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; + currentNodes[node.id] = node; + this.on('input',function(msg) { + node.handled++; + node.messages.push(msg); + node.send(msg); }); - }); - - describe('#start',function() { - var getType; - var getNode; - var credentialsClean; - - var stoppedNodes = {}; - var currentNodes = {}; - var rewiredNodes = {}; - var createCount = 0; - - var TestNode = function(n) { - Node.call(this,n); - createCount++; - var node = this; - this.handled = 0; - this.stopped = false; - currentNodes[node.id] = node; - this.on('input',function(msg) { - node.handled++; - node.send(msg); - }); - this.on('close',function() { + this.on('close',function(done) { + setTimeout(function() { node.stopped = true; stoppedNodes[node.id] = node; delete currentNodes[node.id]; - }); - this.updateWires = function(newWires) { - rewiredNodes[node.id] = node; - node.newWires = newWires; - }; - } - util.inherits(TestNode,Node); + done(); + },50); + }); + } + util.inherits(TestAsyncNode,Node); - before(function() { - getNode = sinon.stub(flows,"get",function(id) { - return currentNodes[id]; - }); - getType = sinon.stub(typeRegistry,"get",function(type) { + before(function() { + getType = sinon.stub(typeRegistry,"get",function(type) { + if (type=="test") { return TestNode; - }); - credentialsClean = sinon.stub(credentials,"clean",function(config){}); - + } else { + return TestAsyncNode; + } }); - after(function() { - getType.restore(); - credentialsClean.restore(); - getNode.restore(); + getNode = sinon.stub(flows,"get",function(id) { + return currentNodes[id]; }); - beforeEach(function() { - currentNodes = {}; - stoppedNodes = {}; - rewiredNodes = {}; - createCount = 0; - }); + }); + 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 nodeCount = 0; + Object.keys(flow.getActiveNodes()).length.should.equal(0); + }); + }); + describe('#start',function() { it("instantiates an initial configuration and stops it",function(done) { - var config = [{id:"1",type:"test",foo:"a",wires:["2"]},{id:"2",type:"test",bar:"b",wires:[["3"]]},{id:"3",type:"test",foo:"a",wires:[]}]; - - var flow = new Flow(config); + var config = flowUtils.parseConfig([ + {id:"t1",type:"tab"}, + {id:"1",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["2"]}, + {id:"2",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["3"]}, + {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"]); flow.start(); + Object.keys(flow.getActiveNodes()).should.have.length(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'); + currentNodes.should.have.a.property("1"); currentNodes.should.have.a.property("2"); currentNodes.should.have.a.property("3"); + currentNodes.should.have.a.property("4"); currentNodes["1"].should.have.a.property("handled",0); currentNodes["2"].should.have.a.property("handled",0); @@ -276,107 +163,268 @@ describe.skip('Flow', function() { currentNodes.should.not.have.a.property("1"); currentNodes.should.not.have.a.property("2"); currentNodes.should.not.have.a.property("3"); + currentNodes.should.not.have.a.property("4"); stoppedNodes.should.have.a.property("1"); stoppedNodes.should.have.a.property("2"); stoppedNodes.should.have.a.property("3"); + stoppedNodes.should.have.a.property("4"); + done(); + }); + }); + + + 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 = [{id:"1",type:"test",foo:"a",wires:["2"]},{id:"2",type:"test",bar:"b",wires:[["3"]]},{id:"3",type:"test",foo:"a",wires:[]}]; + var config = flowUtils.parseConfig([ + {id:"t1",type:"tab"}, + {id:"1",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["2"]}, + {id:"2",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["3"]}, + {id:"3",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]} + ]); - var flow = new Flow(config); + 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 createCount.should.equal(3); - flow.start({rewire:"2"}); + flow.start({rewired:["2"]}); createCount.should.equal(3); rewiredNodes.should.have.a.property("2"); 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; + }) + process.env.NODE_RED_TEST_VALUE = "a-value"; + var config = flowUtils.parseConfig([ + {id:"t1",type:"tab"}, + {id:"1",x:10,y:10,z:"t1",type:"test",foo:"$(NODE_RED_TEST_VALUE)",wires:[]}, + {id:"2",x:10,y:10,z:"t1",type:"test",foo:{a:"$(NODE_RED_TEST_VALUE)"},wires:[]}, + {id:"3",x:10,y:10,z:"t1",type:"test",foo:" $(NODE_RED_TEST_VALUE)",wires:[]}, + {id:"4",x:10,y:10,z:"t1",type:"test",foo:"$(NODE_RED_TEST_VALUE) ",wires:[]}, + {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"]); + flow.start(); + + var activeNodes = flow.getActiveNodes(); + + activeNodes["1"].foo.should.equal("a-value"); + activeNodes["2"].foo.a.should.equal("a-value"); + activeNodes["3"].foo.should.equal(" $(NODE_RED_TEST_VALUE)"); + activeNodes["4"].foo.should.equal("$(NODE_RED_TEST_VALUE) "); + activeNodes["5"].foo.should.equal("$(NODE_RED_TEST_VALUE_NONE)"); + activeNodes["6"].foo[0].should.equal("a-value"); + + flow.stop().then(function() { + done(); + }); + }); }); describe('#stop', function() { - var getType; - var getNode; - var credentialsClean; - var stoppedNodes = {}; - var currentNodes = {}; - - var TestNode = function(n) { - Node.call(this,n); - var node = this; - this.handled = 0; - this.stopped = false; - currentNodes[node.id] = node; - this.on('input',function(msg) { - node.handled++; - node.send(msg); - }); - this.on('close',function() { - node.stopped = true; - stoppedNodes[node.id] = node; - delete currentNodes[node.id]; - }); - } - util.inherits(TestNode,Node); - - var TestAsyncNode = function(n) { - Node.call(this,n); - var node = this; - this.handled = 0; - this.stopped = false; - currentNodes[node.id] = node; - this.on('input',function(msg) { - node.handled++; - node.send(msg); - }); - this.on('close',function(done) { - setTimeout(function() { - node.stopped = true; - stoppedNodes[node.id] = node; - delete currentNodes[node.id]; - done(); - },500); - }); - } - util.inherits(TestAsyncNode,Node); - - before(function() { - getNode = sinon.stub(flows,"get",function(id) { - return currentNodes[id]; - }); - getType = sinon.stub(typeRegistry,"get",function(type) { - if (type=="test") { - return TestNode; - } else { - return TestAsyncNode; - } - }); - credentialsClean = sinon.stub(credentials,"clean",function(config){}); - - }); - after(function() { - getType.restore(); - credentialsClean.restore(); - getNode.restore(); - }); - - beforeEach(function() { - currentNodes = {}; - stoppedNodes = {}; - }); it("stops all nodes",function(done) { - var config = [{id:"1",type:"test",foo:"a",wires:["2"]},{id:"2",type:"asyncTest",bar:"b",wires:[["3"]]},{id:"3",type:"test",foo:"a",wires:[]}]; - var flow = new Flow(config); + var config = flowUtils.parseConfig([ + {id:"t1",type:"tab"}, + {id:"1",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["2"]}, + {id:"2",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["3"]}, + {id:"3",x:10,y:10,z:"t1",type:"asyncTest",foo:"a",wires:[]} + ]); + var flow = Flow.create(config,config.flows["t1"]); flow.start(); + currentNodes.should.have.a.property("1"); currentNodes.should.have.a.property("2"); currentNodes.should.have.a.property("3"); @@ -392,16 +440,21 @@ describe.skip('Flow', function() { }); }); - it("stops nodes specified by diff",function(done) { - var config = [{id:"1",type:"test",foo:"a",wires:["2"]},{id:"2",type:"test",bar:"b",wires:[["3"]]},{id:"3",type:"test",foo:"a",wires:[]}]; - var flow = new Flow(config); + it("stops specified 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:"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"]); flow.start(); currentNodes.should.have.a.property("1"); currentNodes.should.have.a.property("2"); currentNodes.should.have.a.property("3"); - flow.stop({stop:["2"]}).then(function() { + flow.stop(["2"]).then(function() { currentNodes.should.have.a.property("1"); currentNodes.should.not.have.a.property("2"); currentNodes.should.have.a.property("3"); @@ -412,480 +465,532 @@ describe.skip('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"]); - - describe('#diffConfig',function() { - var getType; - var getNode; - var credentialsClean; - - var stoppedNodes = {}; - var currentNodes = {}; - - var TestNode = function(n) { - Node.call(this,n); - var node = this; - this.handled = 0; - this.stopped = false; - currentNodes[node.id] = node; - this.on('input',function(msg) { - node.handled++; - node.send(msg); - }); - this.on('close',function() { - node.stopped = true; - stoppedNodes[node.id] = node; - delete currentNodes[node.id]; - }); - } - util.inherits(TestNode,Node); - - before(function() { - getNode = sinon.stub(flows,"get",function(id) { - return currentNodes[id]; - }); - getType = sinon.stub(typeRegistry,"get",function(type) { - return TestNode; - }); - credentialsClean = sinon.stub(credentials,"clean",function(config){}); - - }); - after(function() { - getType.restore(); - credentialsClean.restore(); getNode.restore(); - }); - - beforeEach(function() { - currentNodes = {}; - stoppedNodes = {}; - }); - - - - - - }); - - describe("#diffConfig",function() { - return; - var getType; - var getNode; - var credentialsClean; - - var stoppedNodes = {}; - var currentNodes = {}; - - var TestNode = function(n) { - Node.call(this,n); - var node = this; - this.handled = 0; - this.stopped = false; - currentNodes[node.id] = node; - this.on('input',function(msg) { - node.handled++; - node.send(msg); - }); - this.on('close',function() { - node.stopped = true; - stoppedNodes[node.id] = node; - delete currentNodes[node.id]; - }); - } - util.inherits(TestNode,Node); - - before(function() { getNode = sinon.stub(flows,"get",function(id) { - return currentNodes[id]; + return flow.getNode(id); }); - getType = sinon.stub(typeRegistry,"get",function(type) { - return TestNode; - }); - credentialsClean = sinon.stub(credentials,"clean",function(config){}); + 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(); + }); }); - after(function() { - getType.restore(); - credentialsClean.restore(); + + + }); + + describe("#handleStatus",function() { + it("passes a status event to the adjacent 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:"test",wires:["3"]}, + {id:"3",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]}, + {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(); - }); - - beforeEach(function() { - currentNodes = {}; - stoppedNodes = {}; - }); - - it('handles an identical configuration', function() { - var config = [{id:"123",type:"test",foo:"a",wires:[]}]; - var newConfig = [{id:"123",type:"test",foo:"a",wires:[]}]; - - var flow = new Flow(config); - flow.start(); - var diffResult = flow.diffConfig(newConfig,"nodes"); - - diffResult.should.have.property("type","nodes"); - diffResult.should.have.property("stop",[]); - diffResult.should.have.property("rewire",[]); - diffResult.should.have.property("config",newConfig); - }); - - - }); - - - describe("#dead",function() { - return; - it("stops all nodes on new full deploy",function(done) { - var config = [{id:"1",type:"test",foo:"a",wires:["2"]},{id:"2",type:"test",bar:"b",wires:[["3"]]},{id:"3",type:"test",foo:"a",wires:[]}]; - var newConfig = [{id:"4",type:"test",foo:"a",wires:["5"]},{id:"5",type:"test",bar:"b",wires:[["6"]]},{id:"6",type:"test",foo:"a",wires:[]}]; - - var flow = new Flow(config); - flow.start(); - - currentNodes.should.have.a.property("1"); - currentNodes.should.have.a.property("2"); - currentNodes.should.have.a.property("3"); - - flow.applyConfig(newConfig).then(function() { - currentNodes.should.not.have.a.property("1"); - currentNodes.should.not.have.a.property("2"); - currentNodes.should.not.have.a.property("3"); - stoppedNodes.should.have.a.property("1"); - stoppedNodes.should.have.a.property("2"); - stoppedNodes.should.have.a.property("3"); - - currentNodes.should.have.a.property("4"); - currentNodes.should.have.a.property("5"); - currentNodes.should.have.a.property("6"); - done(); - }); - }); - return true; - - it("stops only modified nodes on 'nodes' deploy",function(done) { - var config = [{id:"1",type:"test",name:"a",wires:["2"]},{id:"2",type:"test",name:"b",wires:[["3"]]},{id:"3",type:"test",name:"c",wires:[]}]; - var newConfig = clone(config); - newConfig[1].name = "B"; - - var flow = new Flow(config); - flow.start(); - - currentNodes.should.have.a.property("1"); - currentNodes.should.have.a.property("2"); - currentNodes.should.have.a.property("3"); - currentNodes["2"].should.have.a.property("name","b"); - - currentNodes["1"].receive({payload:"test"}); - currentNodes["1"].should.have.a.property("handled",1); - currentNodes["2"].should.have.a.property("handled",1); - currentNodes["3"].should.have.a.property("handled",1); - - - flow.applyConfig(newConfig,"nodes").then(function() { - currentNodes.should.have.a.property("1"); - currentNodes.should.have.a.property("2"); - currentNodes.should.have.a.property("3"); - currentNodes["2"].should.have.a.property("name","B"); - - stoppedNodes.should.not.have.a.property("1"); - stoppedNodes.should.have.a.property("2"); - stoppedNodes.should.not.have.a.property("3"); - stoppedNodes["2"].should.have.a.property("name","b"); - - - currentNodes["1"].receive({payload:"test"}); - currentNodes["1"].should.have.a.property("handled",2); - currentNodes["2"].should.have.a.property("handled",1); - currentNodes["3"].should.have.a.property("handled",2); - - done(); - }); - }); - - it("stops only modified flows on 'flows' deploy",function(done) { - var config = [{id:"1",type:"test",name:"a",wires:["2"]},{id:"2",type:"test",name:"b",wires:[[]]},{id:"3",type:"test",name:"c",wires:[]}]; - var newConfig = clone(config); - newConfig[1].name = "B"; - - var flow = new Flow(config); - flow.start(); - - currentNodes.should.have.a.property("1"); - currentNodes.should.have.a.property("2"); - currentNodes.should.have.a.property("3"); - currentNodes["2"].should.have.a.property("name","b"); - - currentNodes["1"].receive({payload:"test"}); - currentNodes["1"].should.have.a.property("handled",1); - currentNodes["2"].should.have.a.property("handled",1); - currentNodes["3"].should.have.a.property("handled",0); - - currentNodes["3"].receive({payload:"test"}); - currentNodes["3"].should.have.a.property("handled",1); - - flow.applyConfig(newConfig,"flows").then(function() { - currentNodes.should.have.a.property("1"); - currentNodes.should.have.a.property("2"); - currentNodes.should.have.a.property("3"); - currentNodes["2"].should.have.a.property("name","B"); - - stoppedNodes.should.have.a.property("1"); - stoppedNodes.should.have.a.property("2"); - stoppedNodes.should.not.have.a.property("3"); - - stoppedNodes["2"].should.have.a.property("name","b"); - - currentNodes["1"].receive({payload:"test"}); - currentNodes["1"].should.have.a.property("handled",1); - currentNodes["2"].should.have.a.property("handled",1); - - currentNodes["3"].receive({payload:"test"}); - currentNodes["3"].should.have.a.property("handled",2); - - done(); - }); - }); - - it("rewires otherwise unmodified nodes on 'nodes' deploy",function(done) { - var config = [{id:"1",type:"test",name:"a",wires:["2"]},{id:"2",type:"test",name:"b",wires:[[]]},{id:"3",type:"test",name:"c",wires:[]}]; - var newConfig = clone(config); - newConfig[1].wires[0].push("3"); - - var flow = new Flow(config); - flow.start(); - - currentNodes.should.have.a.property("1"); - currentNodes.should.have.a.property("2"); - currentNodes.should.have.a.property("3"); - - currentNodes["1"].receive({payload:"test"}); - currentNodes["1"].should.have.a.property("handled",1); - currentNodes["2"].should.have.a.property("handled",1); - currentNodes["3"].should.have.a.property("handled",0); - - flow.applyConfig(newConfig,"nodes").then(function() { - currentNodes.should.have.a.property("1"); - currentNodes.should.have.a.property("2"); - currentNodes.should.have.a.property("3"); - - stoppedNodes.should.not.have.a.property("1"); - stoppedNodes.should.not.have.a.property("2"); - stoppedNodes.should.not.have.a.property("3"); - - currentNodes["1"].receive({payload:"test"}); - currentNodes["1"].should.have.a.property("handled",2); - currentNodes["2"].should.have.a.property("handled",2); - currentNodes["3"].should.have.a.property("handled",1); - - done(); - }); - }); - - it("stops rewired but otherwise unmodified nodes on 'flows' deploy",function(done) { - var config = [{id:"1",type:"test",name:"a",wires:["2"]},{id:"2",type:"test",name:"b",wires:[[]]},{id:"3",type:"test",name:"c",wires:[]}]; - var newConfig = clone(config); - newConfig[1].wires[0].push("3"); - - var flow = new Flow(config); - flow.start(); - - currentNodes.should.have.a.property("1"); - currentNodes.should.have.a.property("2"); - currentNodes.should.have.a.property("3"); - - currentNodes["1"].receive({payload:"test"}); - currentNodes["1"].should.have.a.property("handled",1); - currentNodes["2"].should.have.a.property("handled",1); - currentNodes["3"].should.have.a.property("handled",0); - - flow.applyConfig(newConfig,"flows").then(function() { - currentNodes.should.have.a.property("1"); - currentNodes.should.have.a.property("2"); - currentNodes.should.have.a.property("3"); - - stoppedNodes.should.have.a.property("1"); - stoppedNodes.should.have.a.property("2"); - stoppedNodes.should.have.a.property("3"); - - currentNodes["1"].receive({payload:"test"}); - currentNodes["1"].should.have.a.property("handled",1); - currentNodes["2"].should.have.a.property("handled",1); - currentNodes["3"].should.have.a.property("handled",1); - - done(); - }); - }); - }); - - describe('#handleError',function() { - var getType; - var getNode; - var credentialsClean; - - var currentNodes = {}; - - var TestNode = function(n) { - Node.call(this,n); - var node = this; - this.handled = []; - currentNodes[node.id] = node; - this.on('input',function(msg) { - node.handled.push(msg); - node.send(msg); - }); - } - util.inherits(TestNode,Node); - - before(function() { getNode = sinon.stub(flows,"get",function(id) { - return currentNodes[id]; + return flow.getNode(id); }); - getType = sinon.stub(typeRegistry,"get",function(type) { - return TestNode; - }); - credentialsClean = sinon.stub(credentials,"clean",function(config){}); + flow.start(); + + var activeNodes = flow.getActiveNodes(); + Object.keys(activeNodes).should.have.length(5); + + + flow.handleStatus(config.flows["t1"].nodes["1"],{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","1"); + statusMessage.status.source.should.have.a.property("type","test"); + statusMessage.status.source.should.have.a.property("name","a"); + + currentNodes["sn2"].should.have.a.property("handled",1); + statusMessage = currentNodes["sn2"].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","1"); + statusMessage.status.source.should.have.a.property("type","test"); + statusMessage.status.source.should.have.a.property("name","a"); + + + flow.stop().then(function() { + done(); + }); }); - after(function() { - getType.restore(); - credentialsClean.restore(); + it("passes a status event to the adjacent scoped 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:"test",wires:["3"]}, + {id:"3",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]}, + {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); + }); - beforeEach(function() { - currentNodes = {}; - }); - - it("reports error to catch nodes on same z",function(done) { - var config = [ - {id:"1",type:"test",z:"tab1",name:"a",wires:["2"]}, - {id:"2",type:"catch",z:"tab1",wires:[[]]}, - {id:"3",type:"catch",z:"tab2",wires:[[]]} - ]; - var flow = new Flow(config); flow.start(); - var msg = {a:1}; - flow.handleError(getNode(1),"test error",msg); - var n2 = getNode(2); - n2.handled.should.have.lengthOf(1); - n2.handled[0].should.have.property("a",1); - n2.handled[0].should.have.property("error"); - n2.handled[0].error.should.have.property("message","test error"); - n2.handled[0].error.should.have.property("source"); - n2.handled[0].error.source.should.have.property("id","1"); - n2.handled[0].error.source.should.have.property("type","test"); - getNode(3).handled.should.have.lengthOf(0); - done(); + + var activeNodes = flow.getActiveNodes(); + Object.keys(activeNodes).should.have.length(5); + + + flow.handleStatus(config.flows["t1"].nodes["1"],{text:"my-status"}); + + currentNodes["sn"].should.have.a.property("handled",0); + currentNodes["sn2"].should.have.a.property("handled",1); + var statusMessage = currentNodes["sn2"].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","1"); + statusMessage.status.source.should.have.a.property("type","test"); + statusMessage.status.source.should.have.a.property("name","a"); + + + flow.stop().then(function() { + done(); + }); }); - it("reports error with Error object",function(done) { - var config = [ - {id:"1",type:"test",z:"tab1",name:"a",wires:["2"]}, - {id:"2",type:"catch",z:"tab1",wires:[[]]}, - {id:"3",type:"catch",z:"tab2",wires:[[]]} - ]; - var flow = new Flow(config); + 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 msg = {a:1,error:"existing"}; - flow.handleError(getNode(1),"test error",msg); - var n2 = getNode(2); - n2.handled.should.have.lengthOf(1); - n2.handled[0].should.have.property("error"); - n2.handled[0].should.have.property("_error","existing"); - n2.handled[0].error.should.have.property("message","test error"); - n2.handled[0].error.should.have.property("source"); - n2.handled[0].error.source.should.have.property("id","1"); - n2.handled[0].error.source.should.have.property("type","test"); - getNode(3).handled.should.have.lengthOf(0); - done(); + + 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:"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)[3]; + var statusInstanceId = Object.keys(activeNodes)[4]; + var statusInstanceId2 = Object.keys(activeNodes)[5]; + var statusInstanceId3 = Object.keys(activeNodes)[6]; + + 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); + + 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(); + }); + }); + }); + + + describe("#handleError",function() { + it("passes an error event to the adjacent 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:"test",wires:["3"]}, + {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:[]}, + {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); + }); + + flow.start(); + + var activeNodes = flow.getActiveNodes(); + Object.keys(activeNodes).should.have.length(5); + + + flow.handleError(config.flows["t1"].nodes["1"],"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","1"); + statusMessage.error.source.should.have.a.property("type","test"); + statusMessage.error.source.should.have.a.property("name","a"); + + currentNodes["sn2"].should.have.a.property("handled",1); + statusMessage = currentNodes["sn2"].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","1"); + statusMessage.error.source.should.have.a.property("type","test"); + statusMessage.error.source.should.have.a.property("name","a"); + + + flow.stop().then(function() { + done(); + }); + }); + it("passes an error event to the adjacent scoped 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:"test",wires:["3"]}, + {id:"3",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]}, + {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); + }); + + flow.start(); + + var activeNodes = flow.getActiveNodes(); + Object.keys(activeNodes).should.have.length(5); + + flow.handleError(config.flows["t1"].nodes["1"],"my-error",{a:"foo"}); + + currentNodes["sn"].should.have.a.property("handled",0); + currentNodes["sn2"].should.have.a.property("handled",1); + var statusMessage = currentNodes["sn2"].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","1"); + statusMessage.error.source.should.have.a.property("type","test"); + statusMessage.error.source.should.have.a.property("name","a"); + + + flow.stop().then(function() { + done(); + }); }); - it("reports error with Error object",function(done) { - var config = [ - {id:"1",type:"test",z:"tab1",name:"a",wires:["2"]}, - {id:"2",type:"catch",z:"tab1",wires:[[]]}, - {id:"3",type:"catch",z:"tab2",wires:[[]]} - ]; - var flow = new Flow(config); + 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(); - flow.handleError(getNode(1),new Error("test error")); - var n2 = getNode(2); - n2.handled.should.have.lengthOf(1); - n2.handled[0].should.have.property("error"); - n2.handled[0].error.should.have.property("message","Error: test error"); - n2.handled[0].error.should.have.property("source"); - n2.handled[0].error.source.should.have.property("id","1"); - n2.handled[0].error.source.should.have.property("type","test"); - getNode(3).handled.should.have.lengthOf(0); - done(); + + 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('reports error in subflow to a local handler', function(done) { - var config = [ - {id:"1",type:"test",z:"tab1",wires:[[]]}, - {id:"2",type:"subflow:sf1",z:"tab1",wires:[[]]}, - {id:"3",type:"catch",z:"tab1",wires:[]}, - {id:"sf1",type:"subflow","in": [],"out": []}, - {id:"sf1-1",z:"sf1",type:"test",wires:[]}, - {id:"sf1-catch",type:"catch",z:"sf1",wires:[]} - ]; - var flow = new Flow(config); - flow.start(); - var instanceNode; - var instanceCatch; - for (var id in currentNodes) { - if (currentNodes.hasOwnProperty(id)) { - if (currentNodes[id].z == '2') { - if (currentNodes[id].type == "catch") { - instanceCatch = currentNodes[id]; - } else { - instanceNode = currentNodes[id]; - } - } - } - } - flow.handleError(instanceNode,new Error("test error")); - var n3 = instanceCatch; - n3.handled.should.have.lengthOf(1); - n3.handled[0].should.have.property("error"); - n3.handled[0].error.should.have.property("message","Error: test error"); - n3.handled[0].error.should.have.property("source"); - n3.handled[0].error.source.should.have.property("id",instanceNode.id); - n3.handled[0].error.source.should.have.property("type","test"); - getNode(3).handled.should.have.lengthOf(0); - done(); - }); - it('reports error in subflow to a parent handler', function(done) { - var config = [ - {id:"1",type:"test",z:"tab1",wires:[[]]}, - {id:"2",type:"subflow:sf1",z:"tab1",wires:[[]]}, - {id:"3",type:"catch",z:"tab1",wires:[]}, - {id:"4",type:"catch",z:"tab2",wires:[]}, - {id:"sf1",type:"subflow","in": [],"out": []}, - {id:"sf1-1",z:"sf1",type:"test",wires:[]} - ]; - var flow = new Flow(config); - flow.start(); - var instanceNode; + 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"]); - for (var id in currentNodes) { - if (currentNodes.hasOwnProperty(id)) { - if (currentNodes[id].z == '2') { - instanceNode = currentNodes[id]; - } - } - } - flow.handleError(instanceNode,new Error("test error")); - var n3 = getNode(3); - n3.handled.should.have.lengthOf(1); - n3.handled[0].should.have.property("error"); - n3.handled[0].error.should.have.property("message","Error: test error"); - n3.handled[0].error.should.have.property("source"); - n3.handled[0].error.source.should.have.property("id",instanceNode.id); - n3.handled[0].error.source.should.have.property("type","test"); - getNode(4).handled.should.have.lengthOf(0); - done(); + 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(){ + 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:"test",wires:["3"]}, + {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); + }); + + flow.start(); + + var activeNodes = flow.getActiveNodes(); + + flow.handleError(config.flows["t1"].nodes["1"],"my-error",{a:"foo",error:"existing"}); + + currentNodes["sn"].should.have.a.property("handled",1); + var statusMessage = currentNodes["sn"].messages[0]; + + statusMessage.should.have.a.property("_error","existing"); + 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","1"); + statusMessage.error.source.should.have.a.property("type","test"); + statusMessage.error.source.should.have.a.property("name","a"); + + flow.stop().then(function() { + done(); + }); + }); + it.skip("prevents an error looping more than 10 times",function(){}); }); }); diff --git a/test/red/nodes/flows/index_spec.js b/test/red/nodes/flows/index_spec.js index 1276de5ad..1983e1986 100644 --- a/test/red/nodes/flows/index_spec.js +++ b/test/red/nodes/flows/index_spec.js @@ -26,30 +26,10 @@ var credentials = require("../../../../red/nodes/credentials"); var typeRegistry = require("../../../../red/nodes/registry"); var Flow = require("../../../../red/nodes/flows/Flow"); -var settings = { - available: function() { return false; } -} - -function loadFlows(testFlows, cb) { - var storage = { - getFlows: function() { - return when.resolve(testFlows); - }, - getCredentials: function() { - return when.resolve({}); - } - }; - RED.init(settings, storage); - flows.load().then(function() { - should.deepEqual(testFlows, flows.getFlows()); - cb(); - }); -} - describe('flows/index', function() { - var eventsOn; var storage; + var eventsOn; var credentialsExtract; var credentialsSave; var credentialsClean; @@ -60,7 +40,7 @@ describe('flows/index', function() { before(function() { getType = sinon.stub(typeRegistry,"get",function(type) { - return type!=='missing'; + return type.indexOf('missing') === -1; }); }); after(function() { @@ -69,7 +49,7 @@ describe('flows/index', function() { beforeEach(function() { - eventsOn = sinon.stub(events,"on",function(evt,handler) {}); + eventsOn = sinon.spy(events,"on"); credentialsExtract = sinon.stub(credentials,"extract",function(conf) { delete conf.credentials; }); @@ -91,9 +71,17 @@ describe('flows/index', function() { id = flow.id; } flowCreate.flows[id] = { + flow: flow, + global: global, start: sinon.spy(), update: sinon.spy(), - stop: sinon.spy() + stop: sinon.spy(), + getActiveNodes: function() { + return flow.nodes||{}; + }, + handleError: sinon.spy(), + handleStatus: sinon.spy() + } return flowCreate.flows[id]; }); @@ -107,20 +95,23 @@ describe('flows/index', function() { } }); - afterEach(function() { + afterEach(function(done) { eventsOn.restore(); credentialsExtract.restore(); credentialsSave.restore(); credentialsClean.restore(); credentialsLoad.restore(); flowCreate.restore(); + + flows.stopFlows().then(done); + }); - describe('#init',function() { - it('registers the type-registered handler', function() { - flows.init({},{}); - eventsOn.calledOnce.should.be.true; - }); - }); + // describe('#init',function() { + // it('registers the type-registered handler', function() { + // flows.init({},{}); + // eventsOn.calledOnce.should.be.true; + // }); + // }); describe('#setFlows', function() { it('sets the full flow', function(done) { @@ -166,12 +157,70 @@ describe('flows/index', function() { credentialsClean.called.should.be.true; storage.hasOwnProperty('conf').should.be.true; var cleanedFlows = flows.getFlows(); + storage.conf.should.eql(cleanedFlows); cleanedFlows.should.not.eql(originalConfig); cleanedFlows[0].credentials = {}; cleanedFlows.should.eql(originalConfig); done(); }); + }); + it('updates existing flows with partial deployment - nodes', function(done) { + var originalConfig = [ + {id:"t1-1",x:10,y:10,z:"t1",type:"test",wires:[]}, + {id:"t1",type:"tab"} + ]; + var newConfig = clone(originalConfig); + newConfig.push({id:"t1-2",x:10,y:10,z:"t1",type:"test",wires:[]}); + newConfig.push({id:"t2",type:"tab"}); + newConfig.push({id:"t2-1",x:10,y:10,z:"t2",type:"test",wires:[]}); + storage.getFlows = function() { + return when.resolve(originalConfig); + } + + events.once('nodes-started',function() { + flows.setFlows(newConfig,"nodes").then(function() { + flows.getFlows().should.eql(newConfig); + flowCreate.flows['t1'].update.called.should.be.true; + flowCreate.flows['t2'].start.called.should.be.true; + flowCreate.flows['_GLOBAL_'].update.called.should.be.true; + done(); + }) + }); + + flows.init({},storage); + flows.load().then(function() { + flows.startFlows(); + }); + }); + + it('updates existing flows with partial deployment - flows', function(done) { + var originalConfig = [ + {id:"t1-1",x:10,y:10,z:"t1",type:"test",wires:[]}, + {id:"t1",type:"tab"} + ]; + var newConfig = clone(originalConfig); + newConfig.push({id:"t1-2",x:10,y:10,z:"t1",type:"test",wires:[]}); + newConfig.push({id:"t2",type:"tab"}); + newConfig.push({id:"t2-1",x:10,y:10,z:"t2",type:"test",wires:[]}); + storage.getFlows = function() { + return when.resolve(originalConfig); + } + + events.once('nodes-started',function() { + flows.setFlows(newConfig,"nodes").then(function() { + flows.getFlows().should.eql(newConfig); + flowCreate.flows['t1'].update.called.should.be.true; + flowCreate.flows['t2'].start.called.should.be.true; + flowCreate.flows['_GLOBAL_'].update.called.should.be.true; + flows.stopFlows().then(done); + }) + }); + + flows.init({},storage); + flows.load().then(function() { + flows.startFlows(); + }); }); }); @@ -195,12 +244,84 @@ describe('flows/index', function() { flows.getFlows().should.eql(originalConfig); done(); }); - }); }); describe('#startFlows', function() { - it.skip('starts the loaded config', function(done) { + it('starts the loaded config', 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(originalConfig); + } + + events.once('nodes-started',function() { + Object.keys(flowCreate.flows).should.eql(['_GLOBAL_','t1']); + done(); + }); + + flows.init({},storage); + flows.load().then(function() { + flows.startFlows(); + }); + }); + it('does not start if nodes missing', function(done) { + var originalConfig = [ + {id:"t1-1",x:10,y:10,z:"t1",type:"test",wires:[]}, + {id:"t1-2",x:10,y:10,z:"t1",type:"missing",wires:[]}, + {id:"t1",type:"tab"} + ]; + storage.getFlows = function() { + return when.resolve(originalConfig); + } + + flows.init({},storage); + flows.load().then(function() { + flows.startFlows(); + flowCreate.called.should.be.false; + done(); + }); + }); + + it('starts when missing nodes registered', function(done) { + var originalConfig = [ + {id:"t1-1",x:10,y:10,z:"t1",type:"test",wires:[]}, + {id:"t1-2",x:10,y:10,z:"t1",type:"missing",wires:[]}, + {id:"t1-3",x:10,y:10,z:"t1",type:"missing2",wires:[]}, + {id:"t1",type:"tab"} + ]; + storage.getFlows = function() { + return when.resolve(originalConfig); + } + flows.init({},storage); + flows.load().then(function() { + flows.startFlows(); + flowCreate.called.should.be.false; + + events.emit("type-registered","missing"); + setTimeout(function() { + flowCreate.called.should.be.false; + events.emit("type-registered","missing2"); + setTimeout(function() { + flowCreate.called.should.be.true; + done(); + },10); + },10); + }); + }); + + + + }); + + describe('#get',function() { + + }); + + describe('#eachNode', function() { + it('iterates the flow nodes', function(done) { var originalConfig = [ {id:"t1-1",x:10,y:10,z:"t1",type:"test",wires:[]}, {id:"t1",type:"tab"} @@ -210,29 +331,62 @@ describe('flows/index', function() { } flows.init({},storage); flows.load().then(function() { - flows.startFlows(); - // TODO: PICK IT UP FROM HERE + var c = 0; + flows.eachNode(function(node) { + c++ + }) + c.should.equal(2); done(); }); }); }); - describe('#get',function() { - - }); - - describe('#eachNode', 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(originalConfig); + } + events.once('nodes-started',function() { + flows.handleError(originalConfig[0],"message",{}); + flowCreate.flows['t1'].handleError.called.should.be.true; + done(); + }); + + flows.init({},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(originalConfig); + } + events.once('nodes-started',function() { + flows.handleStatus(originalConfig[0],"message"); + flowCreate.flows['t1'].handleStatus.called.should.be.true; + done(); + }); + + flows.init({},storage); + flows.load().then(function() { + flows.startFlows(); + }); + }); }); }); diff --git a/test/red/nodes/index_spec.js b/test/red/nodes/index_spec.js index f5d230c47..fd8f2ce74 100644 --- a/test/red/nodes/index_spec.js +++ b/test/red/nodes/index_spec.js @@ -21,8 +21,15 @@ var when = require("when"); var sinon = require('sinon'); var index = require("../../../red/nodes/index"); +var flows = require("../../../red/nodes/flows"); describe("red/nodes/index", function() { + before(function() { + sinon.stub(flows,"startFlows"); + }); + after(function() { + flows.startFlows.restore(); + }); afterEach(function() { index.clearRegistry(); From ab87fa9ce428b27c11803cf71616e68021205b09 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Mon, 2 Nov 2015 20:41:59 +0000 Subject: [PATCH 4/4] 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() {