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..cc35355f3 --- /dev/null +++ b/red/nodes/flows/Flow.js @@ -0,0 +1,466 @@ +/** + * 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(); + } + } + } + }); + typeEventRegistered = true; + } +} +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) { + var config = clone(_config); + type = type||"full"; + + var credentialsChanged = false; + var credentialSavePromise = null; + var configSavePromise = null; + + 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 { + credentialSavePromise = when.resolve(); + } + if (type === 'load') { + configSavePromise = credentialSavePromise; + type = 'full'; + } else { + configSavePromise = credentialSavePromise.then(function() { + return storage.saveFlows(config); + }); + } + + return configSavePromise + .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) { + }) + } + }); + }); +} + +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.allNodes) { + if (activeFlowConfig.allNodes.hasOwnProperty(id)) { + cb(activeFlowConfig.allNodes[id]); + } + } +} + +function getConfig() { + return activeConfig; +} + +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 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) { + 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; + if (activeNodes[nid]._alias) { + subflowInstanceNodeMap[activeNodes[nid]._alias] = subflowInstanceNodeMap[activeNodes[nid]._alias] || []; + subflowInstanceNodeMap[activeNodes[nid]._alias].push(nid); + } + }); + + } + } + 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 (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 { + 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..736078b86 --- /dev/null +++ b/red/nodes/flows/util.js @@ -0,0 +1,327 @@ +/** + * 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/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(); }); diff --git a/test/red/nodes/Flow_spec.js b/test/red/nodes/Flow_spec.js deleted file mode 100644 index 78576dbd4..000000000 --- a/test/red/nodes/Flow_spec.js +++ /dev/null @@ -1,1458 +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 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 typeRegistry = require("../../../red/nodes/registry"); -var credentials = require("../../../red/nodes/credentials"); - - -describe('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 - // 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(); - } - }); - - }); - - 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(); - } - }); - - 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(); - } - }); - - 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(); - } - }); - }); - - 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() { - 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); - - 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 = {}; - 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"); - 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"); - 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 flow = new Flow(config); - createCount.should.equal(0); - flow.start(); - createCount.should.equal(3); - flow.start({rewire:"2"}); - createCount.should.equal(3); - rewiredNodes.should.have.a.property("2"); - 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); - 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"); - 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"); - 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"); - currentNodes.should.have.a.property("3"); - stoppedNodes.should.not.have.a.property("1"); - stoppedNodes.should.have.a.property("2"); - stoppedNodes.should.not.have.a.property("3"); - done(); - }); - }); - - }); - - - 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 = {}; - }); - - - 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() { - 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]; - }); - 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 = {}; - }); - - 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]; - }); - 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 = {}; - }); - - 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(); - }); - - 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); - 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(); - }); - - 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); - 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(); - }); - - 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; - - 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(); - }); - }); -}); \ No newline at end of file diff --git a/test/red/nodes/flows/Flow_spec.js b/test/red/nodes/flows/Flow_spec.js new file mode 100644 index 000000000..727351930 --- /dev/null +++ b/test/red/nodes/flows/Flow_spec.js @@ -0,0 +1,1000 @@ +/** + * 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 clone = require('clone'); +var util = require("util"); + +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('Flow', function() { + var getType; + var getNode; + + var stoppedNodes = {}; + var currentNodes = {}; + var rewiredNodes = {}; + var createCount = 0; + + beforeEach(function() { + currentNodes = {}; + stoppedNodes = {}; + rewiredNodes = {}; + createCount = 0; + }); + + 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); + }); + this.on('close',function() { + node.stopped = true; + stoppedNodes[node.id] = node; + delete currentNodes[node.id]; + }); + this.__updateWires = this.updateWires; + this.updateWires = function(newWires) { + rewiredNodes[node.id] = node; + node.newWires = newWires; + node.__updateWires[newWires]; + }; + } + util.inherits(TestNode,Node); + + var 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); + }); + this.on('close',function(done) { + setTimeout(function() { + node.stopped = true; + stoppedNodes[node.id] = node; + delete currentNodes[node.id]; + done(); + },50); + }); + } + util.inherits(TestAsyncNode,Node); + + before(function() { + getType = sinon.stub(typeRegistry,"get",function(type) { + if (type=="test") { + return TestNode; + } else { + return TestAsyncNode; + } + }); + getNode = sinon.stub(flows,"get",function(id) { + return currentNodes[id]; + }); + + }); + after(function() { + getType.restore(); + getNode.restore(); + }); + + + + describe('#constructor',function() { + it('called with an empty flow',function() { + var config = flowUtils.parseConfig([]); + var flow = Flow.create(config); + + var 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 = 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); + 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"); + 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 = 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"]); + createCount.should.equal(0); + flow.start(); + //TODO: use update to pass in new wiring and verify the change + createCount.should.equal(3); + 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() { + + + it("stops all 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:"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"); + + flow.stop().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"); + done(); + }); + }); + + 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(["2"]).then(function() { + currentNodes.should.have.a.property("1"); + currentNodes.should.not.have.a.property("2"); + currentNodes.should.have.a.property("3"); + stoppedNodes.should.not.have.a.property("1"); + stoppedNodes.should.have.a.property("2"); + stoppedNodes.should.not.have.a.property("3"); + done(); + }); + }); + + it("stops subflow instance nodes",function(done) { + var config = flowUtils.parseConfig([ + {id:"t1",type:"tab"}, + {id:"1",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["2"]}, + {id:"2",x:10,y:10,z:"t1",type:"subflow:sf1",wires:["3"]}, + {id:"3",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]}, + {id:"sf1",type:"subflow","name":"Subflow 2","info":"", + "in":[{"wires":[{"id":"sf1-1"}]}],"out":[{"wires":[{"id":"sf1-1","port":0}]}]}, + {id:"sf1-1",type:"test","z":"sf1",x:166,y:99,"wires":[[]]} + ]); + var flow = Flow.create(config,config.flows["t1"]); + + getNode.restore(); + getNode = sinon.stub(flows,"get",function(id) { + return flow.getNode(id); + }); + + flow.start(); + + var activeNodes = flow.getActiveNodes(); + Object.keys(activeNodes).should.have.length(4); + var sfInstanceId = Object.keys(activeNodes)[3]; + flow.stop(["2"]).then(function() { + currentNodes.should.not.have.a.property(sfInstanceId); + stoppedNodes.should.have.a.property(sfInstanceId); + done(); + }); + }); + + + }); + + 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(); + 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.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(); + }); + }); + 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); + }); + + 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",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("passes a status event to the adjacent status node in subflow",function(done) { + var config = flowUtils.parseConfig([ + {id:"t1",type:"tab"}, + {id:"1",x:10,y:10,z:"t1",type:"test",name:"a",wires:["2"]}, + {id:"2",x:10,y:10,z:"t1",type:"subflow:sf1",wires:["3"]}, + {id:"3",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]}, + {id:"sf1",type:"subflow","name":"Subflow 2","info":"", + "in":[{"wires":[{"id":"sf1-1"}]}],"out":[{"wires":[{"id":"sf1-1","port":0}]}]}, + {id:"sf1-1",type:"test2","z":"sf1",x:166,y:99,"wires":[[]]}, + {id:"sf1-sn",x:10,y:10,z:"sf1",type:"status",foo:"a",wires:[]} + ]); + var flow = Flow.create(config,config.flows["t1"]); + + getNode.restore(); + getNode = sinon.stub(flows,"get",function(id) { + return flow.getNode(id); + }); + + flow.start(); + + var activeNodes = flow.getActiveNodes(); + var sfInstanceId = Object.keys(activeNodes)[3]; + var statusInstanceId = Object.keys(activeNodes)[4]; + + flow.handleStatus(activeNodes[sfInstanceId],{text:"my-status"}); + + currentNodes[statusInstanceId].should.have.a.property("handled",1); + var statusMessage = currentNodes[statusInstanceId].messages[0]; + + statusMessage.should.have.a.property("status"); + statusMessage.status.should.have.a.property("text","my-status"); + statusMessage.status.should.have.a.property("source"); + statusMessage.status.source.should.have.a.property("id",sfInstanceId); + statusMessage.status.source.should.have.a.property("type","test2"); + statusMessage.status.source.should.have.a.property("name",undefined); + flow.stop().then(function() { + + done(); + }); + }); + it("passes a status event to the multiple adjacent status nodes in subflow",function(done) { + var config = flowUtils.parseConfig([ + {id:"t1",type:"tab"}, + {id:"1",x:10,y:10,z:"t1",type:"test",name:"a",wires:["2"]}, + {id:"2",x:10,y:10,z:"t1",type:"subflow:sf1",wires:["3"]}, + {id:"3",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]}, + {id:"4",x:10,y:10,z:"t1",type:"status",foo:"a",wires:[]}, + {id:"sf1",type:"subflow","name":"Subflow 2","info":"", + "in":[{"wires":[{"id":"sf1-1"}]}],"out":[{"wires":[{"id":"sf1-1","port":0}]}]}, + {id:"sf1-1",type:"test2","z":"sf1",x:166,y:99,"wires":[[]]}, + {id:"sf1-sn",x:10,y:10,z:"sf1",type:"status",foo:"a",wires:[]}, + {id:"sf1-sn2",x:10,y:10,z:"sf1",type:"status",scope:["none"],wires:[]}, + {id:"sf1-sn3",x:10,y:10,z:"sf1",type:"status",scope:["sf1-1"],wires:[]} + ]); + var flow = Flow.create(config,config.flows["t1"]); + + getNode.restore(); + getNode = sinon.stub(flows,"get",function(id) { + return flow.getNode(id); + }); + + flow.start(); + + var activeNodes = flow.getActiveNodes(); + + var sfInstanceId = Object.keys(activeNodes)[4]; + var statusInstanceId = Object.keys(activeNodes)[5]; + var statusInstanceId2 = Object.keys(activeNodes)[6]; + var statusInstanceId3 = Object.keys(activeNodes)[7]; + + flow.handleStatus(activeNodes[sfInstanceId],{text:"my-status"}); + + currentNodes[statusInstanceId].should.have.a.property("handled",1); + var statusMessage = currentNodes[statusInstanceId].messages[0]; + + statusMessage.should.have.a.property("status"); + statusMessage.status.should.have.a.property("text","my-status"); + statusMessage.status.should.have.a.property("source"); + statusMessage.status.source.should.have.a.property("id",sfInstanceId); + statusMessage.status.source.should.have.a.property("type","test2"); + statusMessage.status.source.should.have.a.property("name",undefined); + + activeNodes["4"].should.have.a.property("handled",0); + + currentNodes[statusInstanceId2].should.have.a.property("handled",0); + + currentNodes[statusInstanceId3].should.have.a.property("handled",1); + statusMessage = currentNodes[statusInstanceId3].messages[0]; + + statusMessage.should.have.a.property("status"); + statusMessage.status.should.have.a.property("text","my-status"); + statusMessage.status.should.have.a.property("source"); + statusMessage.status.source.should.have.a.property("id",sfInstanceId); + statusMessage.status.source.should.have.a.property("type","test2"); + statusMessage.status.source.should.have.a.property("name",undefined); + + flow.stop().then(function() { + + done(); + }); + }); + it("passes a status event to the subflow's parent tab status node",function(done) { + var config = flowUtils.parseConfig([ + {id:"t1",type:"tab"}, + {id:"1",x:10,y:10,z:"t1",type:"test",name:"a",wires:["2"]}, + {id:"2",x:10,y:10,z:"t1",type:"subflow:sf1",wires:["3"]}, + {id:"3",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]}, + {id:"sf1",type:"subflow","name":"Subflow 2","info":"", + "in":[{"wires":[{"id":"sf1-1"}]}],"out":[{"wires":[{"id":"sf1-1","port":0}]}]}, + {id:"sf1-1",type:"test2","z":"sf1",x:166,y:99,"wires":[[]]}, + {id:"sn",x:10,y:10,z:"t1",type:"status",foo:"a",wires:[]} + ]); + var flow = Flow.create(config,config.flows["t1"]); + + getNode.restore(); + getNode = sinon.stub(flows,"get",function(id) { + return flow.getNode(id); + }); + + flow.start(); + + var activeNodes = flow.getActiveNodes(); + var sfInstanceId = Object.keys(activeNodes)[3]; + + flow.handleStatus(activeNodes[sfInstanceId],{text:"my-status"}); + + currentNodes["sn"].should.have.a.property("handled",1); + var statusMessage = currentNodes["sn"].messages[0]; + + statusMessage.should.have.a.property("status"); + statusMessage.status.should.have.a.property("text","my-status"); + statusMessage.status.should.have.a.property("source"); + statusMessage.status.source.should.have.a.property("id",sfInstanceId); + statusMessage.status.source.should.have.a.property("type","test2"); + statusMessage.status.source.should.have.a.property("name",undefined); + + flow.stop().then(function() { + + done(); + }); + }); + }); + + + 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("passes an error event to the adjacent catch node in subflow",function(done) { + var config = flowUtils.parseConfig([ + {id:"t1",type:"tab"}, + {id:"1",x:10,y:10,z:"t1",type:"test",name:"a",wires:["2"]}, + {id:"2",x:10,y:10,z:"t1",type:"subflow:sf1",wires:["3"]}, + {id:"3",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]}, + {id:"sf1",type:"subflow","name":"Subflow 2","info":"", + "in":[{"wires":[{"id":"sf1-1"}]}],"out":[{"wires":[{"id":"sf1-1","port":0}]}]}, + {id:"sf1-1",type:"test2","z":"sf1",x:166,y:99,"wires":[[]]}, + {id:"sf1-sn",x:10,y:10,z:"sf1",type:"catch",foo:"a",wires:[]} + ]); + var flow = Flow.create(config,config.flows["t1"]); + + getNode.restore(); + getNode = sinon.stub(flows,"get",function(id) { + return flow.getNode(id); + }); + + flow.start(); + + var activeNodes = flow.getActiveNodes(); + var sfInstanceId = Object.keys(activeNodes)[3]; + var catchInstanceId = Object.keys(activeNodes)[4]; + + flow.handleError(activeNodes[sfInstanceId],"my-error",{a:"foo"}); + + currentNodes[catchInstanceId].should.have.a.property("handled",1); + var statusMessage = currentNodes[catchInstanceId].messages[0]; + + statusMessage.should.have.a.property("error"); + statusMessage.error.should.have.a.property("message","my-error"); + statusMessage.error.should.have.a.property("source"); + statusMessage.error.source.should.have.a.property("id",sfInstanceId); + statusMessage.error.source.should.have.a.property("type","test2"); + statusMessage.error.source.should.have.a.property("name",undefined); + + flow.stop().then(function() { + done(); + }); + }); + + it("passes an error event to the multiple adjacent catch nodes in subflow",function(done) { + var config = flowUtils.parseConfig([ + {id:"t1",type:"tab"}, + {id:"1",x:10,y:10,z:"t1",type:"test",name:"a",wires:["2"]}, + {id:"2",x:10,y:10,z:"t1",type:"subflow:sf1",wires:["3"]}, + {id:"3",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]}, + {id:"sf1",type:"subflow","name":"Subflow 2","info":"", + "in":[{"wires":[{"id":"sf1-1"}]}],"out":[{"wires":[{"id":"sf1-1","port":0}]}]}, + {id:"sf1-1",type:"test2","z":"sf1",x:166,y:99,"wires":[[]]}, + {id:"sf1-sn",x:10,y:10,z:"sf1",type:"catch",foo:"a",wires:[]}, + {id:"sf1-sn2",x:10,y:10,z:"sf1",type:"catch",scope:["none"],wires:[]}, + {id:"sf1-sn3",x:10,y:10,z:"sf1",type:"catch",scope:["sf1-1"],wires:[]} + ]); + var flow = Flow.create(config,config.flows["t1"]); + + getNode.restore(); + getNode = sinon.stub(flows,"get",function(id) { + return flow.getNode(id); + }); + + flow.start(); + + var activeNodes = flow.getActiveNodes(); + var sfInstanceId = Object.keys(activeNodes)[3]; + var catchInstanceId = Object.keys(activeNodes)[4]; + var catchInstanceId2 = Object.keys(activeNodes)[5]; + var catchInstanceId3 = Object.keys(activeNodes)[6]; + + flow.handleError(activeNodes[sfInstanceId],"my-error",{a:"foo"}); + + currentNodes[catchInstanceId].should.have.a.property("handled",1); + var statusMessage = currentNodes[catchInstanceId].messages[0]; + + statusMessage.should.have.a.property("error"); + statusMessage.error.should.have.a.property("message","my-error"); + statusMessage.error.should.have.a.property("source"); + statusMessage.error.source.should.have.a.property("id",sfInstanceId); + statusMessage.error.source.should.have.a.property("type","test2"); + statusMessage.error.source.should.have.a.property("name",undefined); + + currentNodes[catchInstanceId2].should.have.a.property("handled",0); + + currentNodes[catchInstanceId3].should.have.a.property("handled",1); + statusMessage = currentNodes[catchInstanceId3].messages[0]; + + statusMessage.should.have.a.property("error"); + statusMessage.error.should.have.a.property("message","my-error"); + statusMessage.error.should.have.a.property("source"); + statusMessage.error.source.should.have.a.property("id",sfInstanceId); + statusMessage.error.source.should.have.a.property("type","test2"); + statusMessage.error.source.should.have.a.property("name",undefined); + + flow.stop().then(function() { + + done(); + }); + }); + it("passes an error event to the subflow's parent tab catch node",function(done) { + var config = flowUtils.parseConfig([ + {id:"t1",type:"tab"}, + {id:"1",x:10,y:10,z:"t1",type:"test",name:"a",wires:["2"]}, + {id:"2",x:10,y:10,z:"t1",type:"subflow:sf1",wires:["3"]}, + {id:"3",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]}, + {id:"sf1",type:"subflow","name":"Subflow 2","info":"", + "in":[{"wires":[{"id":"sf1-1"}]}],"out":[{"wires":[{"id":"sf1-1","port":0}]}]}, + {id:"sf1-1",type:"test2","z":"sf1",x:166,y:99,"wires":[[]]}, + {id:"sn",x:10,y:10,z:"t1",type:"catch",foo:"a",wires:[]} + ]); + var flow = Flow.create(config,config.flows["t1"]); + + getNode.restore(); + getNode = sinon.stub(flows,"get",function(id) { + return flow.getNode(id); + }); + + flow.start(); + + var activeNodes = flow.getActiveNodes(); + var sfInstanceId = Object.keys(activeNodes)[3]; + + flow.handleError(activeNodes[sfInstanceId],"my-error",{a:"foo"}); + + currentNodes["sn"].should.have.a.property("handled",1); + var statusMessage = currentNodes["sn"].messages[0]; + + statusMessage.should.have.a.property("error"); + statusMessage.error.should.have.a.property("message","my-error"); + statusMessage.error.should.have.a.property("source"); + statusMessage.error.source.should.have.a.property("id",sfInstanceId); + statusMessage.error.source.should.have.a.property("type","test2"); + statusMessage.error.source.should.have.a.property("name",undefined); + + flow.stop().then(function() { + done(); + }); + }); + it("moves any existing error object sideways",function(){ + 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 new file mode 100644 index 000000000..d8af1bc09 --- /dev/null +++ b/test/red/nodes/flows/index_spec.js @@ -0,0 +1,454 @@ +/** + * 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"); + +describe('flows/index', function() { + + var storage; + var eventsOn; + var credentialsExtract; + var credentialsSave; + var credentialsClean; + var credentialsLoad; + + var flowCreate; + var getType; + + before(function() { + getType = sinon.stub(typeRegistry,"get",function(type) { + return type.indexOf('missing') === -1; + }); + }); + after(function() { + getType.restore(); + }); + + + beforeEach(function() { + eventsOn = sinon.spy(events,"on"); + 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] = { + flow: flow, + global: global, + start: sinon.spy(), + update: sinon.spy(), + stop: sinon.spy(), + getActiveNodes: function() { + return flow.nodes||{}; + }, + handleError: sinon.spy(), + handleStatus: sinon.spy() + + } + return flowCreate.flows[id]; + }); + flowCreate.flows = {}; + + storage = { + saveFlows: function(conf) { + storage.conf = conf; + return when.resolve(); + } + } + }); + + 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('#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(); + 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(); + }); + }); + + }); + + 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('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"} + ]; + storage.getFlows = function() { + return when.resolve(originalConfig); + } + flows.init({},storage); + flows.load().then(function() { + var c = 0; + flows.eachNode(function(node) { + c++ + }) + c.should.equal(2); + done(); + }); + }); + }); + + 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(); + }); + }); + 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(); + }); + }); + }); + 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(); + }); + }); + + 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(); + }); + }); + }); +}); diff --git a/test/red/nodes/flows/util_spec.js b/test/red/nodes/flows/util_spec.js new file mode 100644 index 000000000..40341ce46 --- /dev/null +++ b/test/red/nodes/flows/util_spec.js @@ -0,0 +1,604 @@ +/** + * 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 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() { + 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/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(); 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() { }); }); }); - + });