msgflo
Version:
Polyglot FBP runtime based on message queues
575 lines (517 loc) • 22.3 kB
text/coffeescript
chai = require 'chai'
EventEmitter = require('events').EventEmitter
websocket = require 'websocket'
fbp = require 'fbp'
fs = require 'fs'
path = require 'path'
participants = require './fixtures/participants'
Runtime = require('../src/runtime').Runtime
rmrf = (dir) ->
return if not fs.existsSync dir
for f in fs.readdirSync dir
f = path.join dir, f
try
fs.unlinkSync f
catch e
if e.code == 'EISDIR'
rmrf f
else
throw e
class MockUi extends EventEmitter
constructor: ->
super()
@connection = null
@client = new websocket.client()
@client.on 'connect', (connection) =>
@connection = connection
@connection.on 'error', (error) =>
throw error
@connection.on 'close', (error) =>
@emit 'disconnected'
@connection.on 'message', (message) =>
@handleMessage message
@emit 'connected', connection
connect: (port) ->
@client.connect "ws://localhost:#{port}/", "noflo"
disconnect: ->
@connection.close() if @connection
@emit 'disconnected'
send: (protocol, command, payload) ->
msg =
protocol: protocol
command: command
payload: payload || {}
@sendMsg msg
sendMsg: (msg) ->
@connection.sendUTF JSON.stringify msg
handleMessage: (message) ->
if not message.type == 'utf8'
throw new Error "Received non-UTF8 message: " + message
d = JSON.parse message.utf8Data
@emit 'message', d, d.protocol, d.command, d.payload
describe 'FBP runtime protocol', () ->
runtime = null
ui = new MockUi
options =
broker: 'mqtt://localhost'
port: 3333
host: 'localhost'
componentdir: 'spec/protocoltemp'
runtimeId: '1b4628d6-a2e0-4873-92ee-eb5c4e4b06f3'
config:
namespace: ''
repository: 'git://github.com/msgflo/msgflo.git'
before (done) ->
rmrf options.componentdir
fs.rmdirSync options.componentdir if fs.existsSync options.componentdir
fs.mkdirSync options.componentdir
comp = fs.readFileSync(__dirname+'/fixtures/ProduceFoo.coffee', 'utf-8')
comp = comp.replace /ProduceFoo/g, 'InitiallyAvailable'
fs.writeFileSync path.join(options.componentdir,'InitiallyAvailable.coffee'), comp
runtime = new Runtime options
runtime.start (err, url) ->
chai.expect(err).to.not.exist
ui.once 'connected', () ->
done()
ui.connect options.port
after (done) ->
ui.once 'disconnected', () ->
runtime.stop () ->
runtime = null
done()
ui.disconnect()
describe 'runtime info', ->
info = null
it 'should be returned on getruntime', (done) ->
ui.send "runtime", "getruntime"
ui.once 'message', (d, protocol, command, payload) ->
info = payload
chai.expect(info).to.be.an 'object'
done()
it 'type should be "msgflo"', ->
chai.expect(info.type).to.equal "msgflo"
it 'protocol version should be "0.7"', ->
chai.expect(info.version).to.be.a "string"
chai.expect(info.version).to.equal "0.7"
describe 'capabilities', ->
it 'should be an array', ->
chai.expect(info.capabilities).to.be.an "array"
it 'should include "protocol:component"', ->
chai.expect(info.capabilities).to.include "protocol:component"
it 'should include "protocol:graph"', ->
chai.expect(info.capabilities).to.include "protocol:graph"
it 'should include "protocol:network"', ->
chai.expect(info.capabilities).to.include "protocol:network"
it 'should include "component:getsource"', ->
chai.expect(info.capabilities).to.include "component:getsource"
it 'should include "component:setsource"', ->
chai.expect(info.capabilities).to.include "component:setsource"
it 'namespace should match namespace from config', ->
chai.expect(info.namespace).to.be.a 'string'
chai.expect(info.namespace).to.equal ''
it 'repository should match repository from config', ->
chai.expect(info.repository).to.be.a 'string'
chai.expect(info.repository).to.contain 'git://'
chai.expect(info.repository).to.contain 'msgflo.git'
it 'runtime id should match options.runtimeId', ->
chai.expect(info.id).to.be.a 'string'
chai.expect(info.id).to.equal options.runtimeId
describe 'participant queues already connected', ->
# TODO: move IIP sending into Participant class?
sendGraphIIPs = (part, graph) ->
processIsParticipant = (name) ->
process = graph.processes[name]
return name == part.definition.id
processes = Object.keys(graph.processes).filter processIsParticipant
iips = graph.connections.filter (c) -> return c.data? and c.tgt.process in processes
for iip in iips
part.send iip.tgt.port, iip.data
it 'should show as connected edges', (done) ->
source = participants.Hello options.broker, 'say'
sink = participants.DevNullSink options.broker, 'sink'
source.definition.outports[0].queue = sink.definition.inports[0].queue
sink.start (err) ->
chai.expect(err).to.be.a 'null'
source.start (err) ->
chai.expect(err).to.be.a 'null'
checkMessage = (d, protocol, command, payload) ->
return if command == 'component' # Ignore component update coming from instantiating
chai.expect(payload).to.be.an 'object'
chai.expect(payload).to.include.keys ['name', 'code', 'language']
chai.expect(payload.language).to.equal 'json'
graph = JSON.parse payload.code
chai.expect(graph).to.include.keys ['connections', 'processes']
chai.expect(graph.connections).to.have.length 1
conn = graph.connections[0]
chai.expect(conn.src.process).to.equal 'say'
chai.expect(conn.src.port).to.equal 'out'
chai.expect(conn.tgt.process).to.equal 'sink'
chai.expect(conn.tgt.port).to.equal 'drop'
ui.removeListener 'message', checkMessage
done()
ui.on 'message', checkMessage
setTimeout () ->
ui.send 'component', 'getsource', { name: 'default/main' }
, 500
describe 'stopping a running network', ->
it 'should succeed'
it 'network:getstatus shows not running'
it 'should not respond to messages'
describe 'starting a stopped network', ->
it 'should succeed'
it 'network:getstatus should show running'
it 'should respond to messages again'
describe 'adding an edge', ->
repeatA = null
repeatB = null
before (done) ->
repeatA = participants.Repeat options.broker, 'addedge-repeat-A'
repeatA.start (err) ->
return done err if err
repeatB = participants.Repeat options.broker, 'addedge-repeat-B'
return repeatB.start done
after (done) ->
repeatA.stop (err) ->
return repeatB.stop done
it 'should succeed', (done) ->
edge =
src: { node: 'addedge-repeat-A', port: 'out' }
tgt: { node: 'addedge-repeat-B', port: 'in' }
metadata:
route: 1
ui.once 'message', (d, protocol, command, payload) ->
chai.expect(payload).to.be.a 'object'
chai.expect(command, JSON.stringify(payload)).to.equal 'addedge'
chai.expect(protocol).to.equal 'graph'
chai.expect(payload).to.have.keys ['src', 'tgt', 'metadata']
chai.expect(payload.src.node).to.equal edge.src.node
chai.expect(payload.src.port).to.equal edge.src.port
chai.expect(payload.tgt.node).to.equal edge.tgt.node
chai.expect(payload.tgt.port).to.equal edge.tgt.port
chai.expect(payload.metadata).to.eql edge.metadata
return done()
ui.send 'graph', 'addedge', edge
it 'data should now be forwarded'
it 'should be possible to change edge metadata', (done) ->
checkMessage = (d, protocol, command, payload) ->
chai.expect(command, JSON.stringify(payload)).to.equal 'changeedge'
chai.expect(protocol).to.equal 'graph'
chai.expect(payload).to.be.an 'object'
chai.expect(payload).to.include.keys ['src', 'tgt', 'metadata']
chai.expect(payload.metadata).to.eql change.metadata
ui.removeListener 'message', checkMessage
done()
ui.on 'message', checkMessage
change =
src: { node: 'addedge-repeat-A', port: 'out' }
tgt: { node: 'addedge-repeat-B', port: 'in' }
metadata:
route: 5
ui.send 'graph', 'changeedge', change
describe 'removing a connected edge', ->
repeatA = null
repeatB = null
before (done) ->
repeatA = participants.Repeat options.broker, 'removeedge-repeat-A'
repeatA.start (err) ->
return done err if err
repeatB = participants.Repeat options.broker, 'removeedge-repeat-B'
repeatB.start (err) ->
edge =
src: { node: 'removeedge-repeat-A', port: 'out' }
tgt: { node: 'removeedge-repeat-B', port: 'in' }
check = (d, protocol, command, payload) ->
return if command == 'component'
chai.expect(command).to.equal 'addedge'
ui.removeListener 'message', check
return done()
ui.on 'message', check
ui.send 'graph', 'addedge', edge
after (done) ->
repeatA.stop (err) ->
return repeatB.stop done
it 'should succeed', (done) ->
edge =
src: { node: 'removeedge-repeat-A', port: 'out' }
tgt: { node: 'removeedge-repeat-B', port: 'in' }
ui.once 'message', (d, protocol, command, payload) ->
chai.expect(payload).to.be.a 'object'
chai.expect(command, JSON.stringify(payload)).to.equal 'removeedge'
chai.expect(protocol).to.equal 'graph'
chai.expect(payload).to.have.keys ['src', 'tgt']
chai.expect(payload.src.node).to.equal edge.src.node
chai.expect(payload.src.port).to.equal edge.src.port
chai.expect(payload.tgt.node).to.equal edge.tgt.node
chai.expect(payload.tgt.port).to.equal edge.tgt.port
return done()
ui.send 'graph', 'removeedge', edge
it 'data should now not be forwarded'
describe 'adding and removing a node', ->
responses = []
nodeName = 'add-remove-node'
componentName = 'InitiallyAvailable'
onNewMessage = null
checkMessage = (d, protocol, command, payload) ->
responses.push
protocol: protocol
command: command
payload: payload
if onNewMessage
onNewMessage()
beforeEach () ->
responses = []
ui.on 'message', checkMessage
afterEach () ->
ui.removeListener 'message', checkMessage
onNewMessage = null
it 'adding should have one addnode response', (done) ->
onNewMessage = () ->
addnodes = responses.filter (r) -> r.protocol == 'graph' and r.command == 'addnode'
return if not addnodes.length # still waiting
chai.expect(addnodes, JSON.stringify(responses)).to.have.length 1
addnode = addnodes[0].payload
chai.expect(addnode).to.include.keys ['id', 'graph', 'component']
chai.expect(addnode.id).to.equal nodeName
chai.expect(addnode.component).to.equal componentName
return done()
node =
id: nodeName
graph: 'default/main'
component: componentName
ui.send 'graph', 'addnode', node
it 'removing should have one removenode response', (done) ->
onNewMessage = () ->
removenodes = responses.filter (r) -> r.protocol == 'graph' and r.command == 'removenode'
return if not removenodes.length # still waiting
chai.expect(removenodes, JSON.stringify(responses)).to.have.length 1
response = removenodes[0].payload
chai.expect(response).to.include.keys ['id', 'graph', 'component']
chai.expect(response.id).to.equal nodeName
chai.expect(response.component).to.equal componentName
return done()
remove =
id: nodeName
graph: 'default/main'
component: componentName
ui.send 'graph', 'removenode', remove
it 'after removing should not be in graph source', (done) ->
onNewMessage = () ->
sources = responses.filter (r) -> r.protocol == 'component' and r.command == 'source'
return if not sources.length # still waiting
payload = sources[0].payload
chai.expect(payload).to.include.keys ['name', 'code', 'language']
chai.expect(payload.language).to.equal 'json'
graph = JSON.parse payload.code
chai.expect(graph).to.include.keys ['processes']
chai.expect(graph.processes).to.not.include.keys [ nodeName ]
return done()
ui.send 'component', 'getsource',
name: 'default/main'
describe 'adding a component', ->
componentName = 'SetSource'
componentCode = fs.readFileSync(__dirname+'/fixtures/ProduceFoo.coffee', 'utf-8')
componentCode = componentCode.replace(/ProduceFoo/g, componentName)
it 'should become available', (done) ->
receivedAck = false
receivedComponent = false
checkMessage = (d, protocol, command, payload) ->
chai.expect(payload).to.be.an 'object'
if command == 'source'
# ACK
chai.expect(protocol).to.equal 'component'
chai.expect(payload).to.include.keys ['name', 'code', 'language', 'library']
chai.expect(payload.language).to.equal 'coffeescript'
chai.expect(payload.code).to.include "component: '#{componentName}'"
chai.expect(payload.code).to.include "module.exports = #{componentName}"
receivedAck = true
console.log 'got ACK'
else if command == 'component'
# New component
chai.expect(protocol).to.equal 'component'
chai.expect(payload).to.include.keys ['name', 'subgraph', 'inPorts', 'outPorts']
chai.expect(payload.name).to.equal componentName
receivedComponent = true
console.log 'got Component'
else
chai.expect(command, "Unexpected command").to.not.exist
if receivedAck and receivedComponent
ui.removeListener 'message', checkMessage
done()
ui.on 'message', checkMessage
source =
name: componentName
library: options.config.namespace
language: 'coffeescript'
code: componentCode
ui.send 'component', 'source', source
it 'should be returned on getsource', (done) ->
ui.once 'message', (d, protocol, command, payload) ->
chai.expect(payload).to.be.an 'object'
chai.expect(command, JSON.stringify(payload)).to.equal 'source'
chai.expect(protocol).to.equal 'component'
chai.expect(payload).to.include.keys ['name', 'code', 'language']
chai.expect(payload.library).to.equal options.config.namespace
chai.expect(payload.name).to.equal 'SetSource'
chai.expect(payload.language).to.equal 'coffeescript'
chai.expect(payload.code).to.include "component: '#{componentName}'"
chai.expect(payload.code).to.include "module.exports = #{componentName}"
done()
source =
name: componentName
ui.send 'component', 'getsource', source
it 'should be instantiable as new node', (done) ->
checkMessage = (d, protocol, command, payload) ->
return if command == 'component' # Ignore component update coming from instantiating
chai.expect(command, JSON.stringify(payload)).to.equal 'addnode'
chai.expect(protocol).to.equal 'graph'
chai.expect(payload).to.be.an 'object'
chai.expect(payload).to.include.keys ['id', 'graph', 'component']
chai.expect(payload.component).to.equal componentName
chai.expect(payload.metadata).to.eql add.metadata
ui.removeListener 'message', checkMessage
done()
ui.on 'message', checkMessage
add =
id: 'mycoffeescriptproducer'
graph: 'default/main'
component: componentName
metadata:
label: 'myproducer'
ui.send 'graph', 'addnode', add
it 'should be possible to change node metadata', (done) ->
checkMessage = (d, protocol, command, payload) ->
chai.expect(command, JSON.stringify(payload)).to.equal 'changenode'
chai.expect(protocol).to.equal 'graph'
chai.expect(payload).to.be.an 'object'
chai.expect(payload).to.include.keys ['id', 'graph', 'metadata']
chai.expect(payload.metadata).to.eql change.metadata
ui.removeListener 'message', checkMessage
done()
ui.on 'message', checkMessage
change =
id: 'mycoffeescriptproducer'
graph: 'default/main'
metadata:
label: 'mycoffeeproducer'
x: 2
ui.send 'graph', 'changenode', change
it 'should include node metadata in JSON result', (done) ->
checkMessage = (d, protocol, command, payload) ->
return unless protocol is 'component'
ui.removeListener 'message', checkMessage
chai.expect(command, JSON.stringify(payload)).to.equal 'source'
chai.expect(payload).to.include.keys ['name', 'code', 'language']
chai.expect(payload.language).to.equal 'json'
graph = JSON.parse payload.code
chai.expect(graph).to.include.keys ['connections', 'processes']
chai.expect(graph.processes).to.include.keys ['mycoffeescriptproducer']
chai.expect(graph.processes.mycoffeescriptproducer).to.eql
component: componentName
metadata:
label: 'mycoffeeproducer'
x: 2
done()
ui.on 'message', checkMessage
ui.send 'component', 'getsource',
name: 'default/main'
describe 'subscribing to edges', ->
repeatA = null
repeatB = null
before (done) ->
# TODO: use addnode to setup instead
repeatA = participants.Repeat options.broker, 'edgedata-repeat-A'
repeatA.start (err) ->
return done err if err
repeatB = participants.Repeat options.broker, 'edgedata-repeat-B'
return repeatB.start done
after (done) ->
repeatA.stop (err) ->
return repeatB.stop done
it 'should emit data flowing through network', (done) ->
edge =
src: { node: 'edgedata-repeat-A', port: 'out' }
tgt: { node: 'edgedata-repeat-B', port: 'in' }
ui.send 'graph', 'addedge', edge
indata =
foo: 'subscribe-edge-11'
onNetwork = (d, protocol, command, payload) ->
chai.expect(payload).to.be.a 'object'
if command == 'edges'
repeatA.send 'in', indata
else if command == 'data'
chai.expect(payload.src).to.eql edge.src
chai.expect(payload.tgt).to.eql edge.tgt
chai.expect(payload.data).to.eql indata
ui.removeListener 'network', onNetwork
return done null
ui.on 'message', onNetwork
subscribe =
edges: [ edge ]
ui.send 'network', 'edges', subscribe
describe 'adding an node and immediately a IIP', ->
# stresses the case where we probably don't have complete information about the added node yet
# as the discovery message will take a bit of time.
# When using Flowhub in project mode this is a likely case to happen. Probably also fbp-spec
responses = []
componentName = 'InitiallyAvailable'
before (done) ->
@timeout 10*1000
checkMessage = (d, protocol, command, payload) ->
return if command == 'component' # Ignore component update coming from instantiating
responses.push
protocol: protocol
command: command
payload: payload
expected = responses.filter (r) -> r.command in ['addinitial', 'addnode']
if expected.length >= 2
ui.removeListener 'message', checkMessage
done()
ui.on 'message', checkMessage
node =
id: 'iip-target'
graph: 'default/main'
component: componentName
initial =
tgt: { node: node.id, port: 'interval' },
src: { data: 0 }
ui.send 'graph', 'addnode', node
ui.send 'graph', 'addinitial', initial
it 'should have one addnode response', ->
addnodes = responses.filter (r) -> r.protocol == 'graph' and r.command == 'addnode'
chai.expect(addnodes, JSON.stringify(responses)).to.have.length 1
addnode = addnodes[0].payload
chai.expect(addnode).to.include.keys ['id', 'graph', 'component']
it 'should have one addinitial response', ->
addinitials = responses.filter (r) -> r.protocol == 'graph' and r.command == 'addinitial'
chai.expect(addinitials, JSON.stringify(responses)).to.have.length 1
addinitial = addinitials[0].payload
chai.expect(addinitial).to.include.keys ['tgt', 'src']
chai.expect(addinitial.tgt).to.include.keys ['node', 'port']
chai.expect(addinitial.tgt.node).to.equal 'iip-target'
chai.expect(addinitial.tgt.port).to.equal 'interval'
it 'clearing the graph should remove processes and IIPs', (done) ->
responses = []
graphName = 'default/main'
checkMessage = (d, protocol, command, payload) ->
responses.push
protocol: protocol
command: command
payload: payload
if command == 'clear'
chai.expect(payload).to.include.keys ['id']
chai.expect(payload.id).to.equal graphName
ui.send 'component', 'getsource', { name: graphName }
else if command == 'source'
chai.expect(payload).to.include.keys ['name', 'code', 'language']
chai.expect(payload.name).to.equal 'main'
graph = JSON.parse payload.code
roles = Object.keys graph.processes
inports = Object.keys graph.inports
outports = Object.keys graph.outports
chai.expect(roles, JSON.stringify(roles)).to.have.length 0
chai.expect(graph.connections, JSON.stringify(graph.connections)).to.have.length 0
chai.expect(inports).to.have.length 0
chai.expect(outports).to.have.length 0
return done()
ui.on 'message', checkMessage
ui.send 'graph', 'clear', { id: graphName }