1 | chai = require 'chai'
|
2 | path = require 'path'
|
3 | { spawn } = require 'child_process'
|
4 | WebSocketClient = require('websocket').client
|
5 | semver = require 'semver'
|
6 | tv4 = require '../schema/index.js'
|
7 |
|
8 | validateSchema = (payload, schema) ->
|
9 | res = tv4.validateMultiple payload, schema
|
10 | chai.expect(res.errors, "#{schema} should produce no errors").to.eql []
|
11 | chai.expect(res.valid, "#{schema} should validate").to.equal true
|
12 | return res.valid
|
13 |
|
14 | exports.testRuntime = (runtimeType, startServer, stopServer, host='localhost', port=8080, collection='core', version='0.7') ->
|
15 | if version.length is 3
|
16 | semanticVersion = "#{version}.0"
|
17 | else
|
18 | semanticVersion = version
|
19 |
|
20 | address = "ws://#{host}:#{port}/"
|
21 | describe "#{runtimeType} webSocket network runtime version #{version}", ->
|
22 | client = null
|
23 | connection = null
|
24 | capabilities = []
|
25 | send = null
|
26 | describe "Connecting to the runtime at #{address}", ->
|
27 | it 'should succeed', (done) ->
|
28 | @timeout 4000
|
29 | tries = 10
|
30 | startServer (err) ->
|
31 | return done err if err
|
32 | client = new WebSocketClient
|
33 | onConnected = (conn) ->
|
34 | connection = conn
|
35 | connection.setMaxListeners(1000)
|
36 | client.removeListener 'connectFailed', onFailed
|
37 | done()
|
38 | onFailed = (err) ->
|
39 | tries--
|
40 | unless tries
|
41 | client.removeListener 'connect', onConnected
|
42 | client.removeListener 'connectFailed', onFailed
|
43 | done err
|
44 | return
|
45 | setTimeout ->
|
46 | client.connect address, 'noflo'
|
47 | , 100
|
48 | client.once 'connect', onConnected
|
49 | client.on 'connectFailed', onFailed
|
50 | client.connect address, 'noflo'
|
51 | after (done) ->
|
52 | unless connection
|
53 | stopServer done
|
54 | return
|
55 | connection.once 'close', ->
|
56 | connection = null
|
57 | stopServer done
|
58 | connection.drop()
|
59 |
|
60 | send = (protocol, command, payload) ->
|
61 | payload = {} unless payload
|
62 |
|
63 | payload.secret = process.env.FBP_PROTOCOL_SECRET
|
64 | connection.sendUTF JSON.stringify
|
65 | protocol: protocol
|
66 | command: command
|
67 | payload: payload
|
68 | secret: process.env.FBP_PROTOCOL_SECRET
|
69 |
|
70 | messageMatches = (msg, expected) ->
|
71 | return false unless msg.protocol is expected.protocol
|
72 | return false unless msg.command is expected.command
|
73 | true
|
74 | getPacketSchema = (packet) ->
|
75 | return "#{packet.protocol}/output/#{packet.command}"
|
76 |
|
77 | receive = (expects, done, allowExtraPackets = false) ->
|
78 | listener = (message) ->
|
79 |
|
80 | chai.expect(message.utf8Data).to.be.a 'string'
|
81 | data = JSON.parse message.utf8Data
|
82 | chai.expect(data.protocol).to.be.a 'string'
|
83 | chai.expect(data.command).to.be.a 'string'
|
84 |
|
85 |
|
86 | delete data.payload.secret
|
87 |
|
88 |
|
89 | validateSchema data, getPacketSchema data
|
90 |
|
91 | chai.expect(data.secret, 'Message should not contain secret').to.be.a 'undefined'
|
92 | chai.expect(data.payload.secret, 'Payload should not contain secret').to.be.a 'undefined'
|
93 |
|
94 | if expects[0].command isnt 'error' and data.command is 'error'
|
95 |
|
96 | done new Error data.payload
|
97 | return
|
98 |
|
99 | if allowExtraPackets and not messageMatches data, expects[0]
|
100 |
|
101 | connection.once 'message', listener
|
102 | return
|
103 |
|
104 |
|
105 | expected = expects.shift()
|
106 | chai.expect(getPacketSchema(data)).to.equal getPacketSchema expected
|
107 |
|
108 |
|
109 |
|
110 | if data.protocol is 'network'
|
111 | if data.command is 'started'
|
112 | delete data.payload.time
|
113 | delete expected.payload.time
|
114 | delete data.payload.running
|
115 | delete expected.payload.running
|
116 | if data.command is 'stopped'
|
117 | delete data.payload.time
|
118 | delete expected.payload.time
|
119 | delete data.payload.uptime
|
120 | delete expected.payload.uptime
|
121 | if data.command is 'error'
|
122 | delete data.payload.stack
|
123 | delete expected.payload.stack
|
124 |
|
125 |
|
126 | delete expected.payload.secret
|
127 |
|
128 | delete expected.secret
|
129 |
|
130 | chai.expect(data.payload).to.eql expected.payload
|
131 |
|
132 | return done() unless expects.length
|
133 |
|
134 | connection.once 'message', listener
|
135 | return
|
136 | connection.once 'message', listener
|
137 |
|
138 | describe 'Runtime Protocol', ->
|
139 | before ->
|
140 | chai.expect(connection, 'Connection needs to be available').not.be.a 'null'
|
141 | describe 'requesting runtime metadata', ->
|
142 | it 'should provide it back', (done) ->
|
143 | connection.once 'message', (message) ->
|
144 | data = JSON.parse message.utf8Data
|
145 | validateSchema data, 'runtime/output/runtime'
|
146 | capabilities = data.payload.capabilities
|
147 | done()
|
148 |
|
149 | send 'runtime', 'getruntime', {}
|
150 |
|
151 | describe 'Graph Protocol', ->
|
152 | before ->
|
153 | chai.expect(connection, 'Connection needs to be available').not.be.a 'null'
|
154 | chai.expect(capabilities, 'Graph protocol should be allowed for user').to.contain 'protocol:graph'
|
155 | describe 'adding a graph and nodes', ->
|
156 | it 'should provide the nodes back', (done) ->
|
157 | expects1 = [
|
158 | protocol: 'graph'
|
159 | command: 'clear',
|
160 | payload:
|
161 | id: 'foo'
|
162 | main: true
|
163 | ]
|
164 | expects2 = [
|
165 | protocol: 'graph'
|
166 | command: 'addnode'
|
167 | payload:
|
168 | id: 'Repeat1'
|
169 | component: "#{collection}/Repeat"
|
170 | metadata:
|
171 | hello: 'World'
|
172 | graph: 'foo'
|
173 | ,
|
174 | protocol: 'graph'
|
175 | command: 'addnode'
|
176 | payload:
|
177 | id: 'Drop1'
|
178 | component: "#{collection}/Drop"
|
179 | metadata: {}
|
180 | graph: 'foo'
|
181 | ]
|
182 | receive expects1, (err) ->
|
183 | return done err if err
|
184 | receive expects2, done, true
|
185 | send 'graph', 'addnode', expects2[0].payload
|
186 | send 'graph', 'addnode', expects2[1].payload
|
187 | , true
|
188 | send 'graph', 'clear',
|
189 | baseDir: path.resolve __dirname, '../'
|
190 | id: 'foo'
|
191 | main: true
|
192 |
|
193 | describe 'adding an edge', ->
|
194 | it 'should provide the edge back', (done) ->
|
195 | expects = [
|
196 | protocol: 'graph'
|
197 | command: 'addedge'
|
198 | payload:
|
199 | src:
|
200 | node: 'Repeat1'
|
201 | port: 'out'
|
202 | tgt:
|
203 | node: 'Drop1'
|
204 | port: 'in'
|
205 | metadata:
|
206 | route: 5
|
207 | graph: 'foo'
|
208 | ]
|
209 | receive expects, done, true
|
210 | send 'graph', 'addedge', expects[0].payload
|
211 |
|
212 |
|
213 |
|
214 |
|
215 |
|
216 |
|
217 |
|
218 |
|
219 |
|
220 |
|
221 |
|
222 |
|
223 |
|
224 |
|
225 |
|
226 |
|
227 |
|
228 |
|
229 |
|
230 |
|
231 |
|
232 |
|
233 |
|
234 |
|
235 |
|
236 |
|
237 |
|
238 |
|
239 |
|
240 |
|
241 |
|
242 |
|
243 |
|
244 |
|
245 |
|
246 |
|
247 |
|
248 |
|
249 |
|
250 |
|
251 | describe 'adding metadata', ->
|
252 | describe 'to a node with no metadata', ->
|
253 | it 'should add the metadata', (done) ->
|
254 | expects = [
|
255 | protocol: 'graph'
|
256 | command: 'changenode'
|
257 | payload:
|
258 | id: 'Drop1'
|
259 | metadata:
|
260 | sort: 1
|
261 | graph: 'foo'
|
262 | ]
|
263 | receive expects, done, true
|
264 | send 'graph', 'changenode', expects[0].payload
|
265 |
|
266 | describe 'to a node with existing metadata', ->
|
267 | it 'should merge the metadata', (done) ->
|
268 | expects = [
|
269 | protocol: 'graph'
|
270 | command: 'changenode'
|
271 | payload:
|
272 | id: 'Drop1'
|
273 | metadata:
|
274 | sort: 1
|
275 | tag: 'awesome'
|
276 | graph: 'foo'
|
277 | ]
|
278 | receive expects, done, true
|
279 | send 'graph', 'changenode',
|
280 | id: 'Drop1'
|
281 | metadata:
|
282 | tag: 'awesome'
|
283 | graph: 'foo'
|
284 |
|
285 | describe 'with no keys to a node with existing metadata', ->
|
286 | it 'should not change the metadata', (done) ->
|
287 | expects = [
|
288 | protocol: 'graph'
|
289 | command: 'changenode'
|
290 | payload:
|
291 | id: 'Drop1'
|
292 | metadata:
|
293 | sort: 1
|
294 | tag: 'awesome'
|
295 | graph: 'foo'
|
296 | ]
|
297 | receive expects, done, true
|
298 | send 'graph', 'changenode',
|
299 | id: 'Drop1'
|
300 | metadata: {}
|
301 | graph: 'foo'
|
302 |
|
303 | describe 'with a null value removes it from the node', ->
|
304 | it 'should merge the metadata', (done) ->
|
305 | expects = [
|
306 | protocol: 'graph'
|
307 | command: 'changenode'
|
308 | payload:
|
309 | id: 'Drop1'
|
310 | metadata: {}
|
311 | graph: 'foo'
|
312 | ]
|
313 | receive expects, done, true
|
314 | send 'graph', 'changenode',
|
315 | id: 'Drop1'
|
316 | metadata:
|
317 | sort: null
|
318 | tag: null
|
319 | graph: 'foo'
|
320 |
|
321 | describe 'adding an IIP', ->
|
322 | it 'should provide the IIP back', (done) ->
|
323 | expects = [
|
324 | protocol: 'graph'
|
325 | command: 'addinitial'
|
326 | payload:
|
327 | src:
|
328 | data: 'Hello, world!'
|
329 | tgt:
|
330 | node: 'Repeat1'
|
331 | port: 'in'
|
332 | metadata: {}
|
333 | graph: 'foo'
|
334 | ]
|
335 | receive expects, done, true
|
336 | send 'graph', 'addinitial', expects[0].payload
|
337 |
|
338 | describe 'removing a node', ->
|
339 | it 'should remove the node and its associated edges', (done) ->
|
340 | expects = [
|
341 | protocol: 'graph'
|
342 | command: 'removeedge'
|
343 | payload:
|
344 | src:
|
345 | node: 'Repeat1'
|
346 | port: 'out'
|
347 | tgt:
|
348 | node: 'Drop1'
|
349 | port: 'in'
|
350 | graph: 'foo'
|
351 | ,
|
352 | protocol: 'graph'
|
353 | command: 'removenode'
|
354 | payload:
|
355 | id: 'Drop1'
|
356 | graph: 'foo'
|
357 | ]
|
358 |
|
359 |
|
360 | receive expects, done, true
|
361 | send 'graph', 'removenode',
|
362 | id: 'Drop1'
|
363 | graph: 'foo'
|
364 |
|
365 | describe 'removing an IIP', ->
|
366 | it 'should provide response that iip was removed', (done) ->
|
367 | expects = [
|
368 | protocol: 'graph'
|
369 | command: 'removeinitial'
|
370 | payload:
|
371 | src:
|
372 | data: 'Hello, world!'
|
373 | tgt:
|
374 | node: 'Repeat1'
|
375 | port: 'in'
|
376 | graph: 'foo'
|
377 | ]
|
378 | receive expects, done, true
|
379 | send 'graph', 'removeinitial',
|
380 | tgt:
|
381 | node: 'Repeat1'
|
382 | port: 'in'
|
383 | graph: 'foo'
|
384 |
|
385 | describe 'renaming a node', ->
|
386 | it 'should send the renamenode event', (done) ->
|
387 | expects = [
|
388 | protocol: 'graph'
|
389 | command: 'renamenode'
|
390 | payload:
|
391 | from: 'Repeat1'
|
392 | to: 'RepeatRenamed'
|
393 | graph: 'foo'
|
394 | ]
|
395 | receive expects, done, true
|
396 | send 'graph', 'renamenode', expects[0].payload
|
397 |
|
398 | describe 'adding a node to a non-existent graph', ->
|
399 | it 'should send an error', (done) ->
|
400 | expects = [
|
401 | protocol: 'graph',
|
402 | command: 'error',
|
403 | payload:
|
404 | message: 'Requested graph not found'
|
405 | ]
|
406 | receive expects, done, true
|
407 | send 'graph', 'addnode',
|
408 | id: 'Repeat1'
|
409 | component: "#{collection}/Repeat"
|
410 | graph: 'another-graph'
|
411 |
|
412 | describe 'adding a node without specifying a graph', ->
|
413 | it 'should send an error', (done) ->
|
414 | expects = [
|
415 | protocol: 'graph',
|
416 | command: 'error',
|
417 | payload:
|
418 | message: 'No graph specified'
|
419 | ]
|
420 | receive expects, done, true
|
421 | send 'graph', 'addnode',
|
422 | id: 'Repeat1'
|
423 | component: "#{collection}/Repeat"
|
424 |
|
425 | describe 'adding an in-port to a graph', ->
|
426 | it "should ACK", (done) ->
|
427 | expects = [
|
428 | protocol: 'graph',
|
429 | command: 'addinport',
|
430 | payload:
|
431 | node: 'RepeatRenamed'
|
432 | graph: 'foo'
|
433 | public: 'in'
|
434 | port: 'in'
|
435 | ]
|
436 | receive expects, done, true
|
437 | send 'graph', 'addinport',
|
438 | public: 'in'
|
439 | node: 'RepeatRenamed'
|
440 | port: 'in'
|
441 | graph: 'foo'
|
442 |
|
443 |
|
444 |
|
445 |
|
446 |
|
447 |
|
448 |
|
449 |
|
450 |
|
451 |
|
452 |
|
453 |
|
454 |
|
455 |
|
456 | describe 'adding an out-port to a graph', ->
|
457 | it "should ACK", (done) ->
|
458 | expects = [
|
459 | protocol: 'graph',
|
460 | command: 'addoutport',
|
461 | payload:
|
462 | graph: 'foo'
|
463 | node: 'RepeatRenamed'
|
464 | port: 'out'
|
465 | public: 'out'
|
466 | ]
|
467 | receive expects, done, true
|
468 | send 'graph', 'addoutport',
|
469 | public: 'out'
|
470 | node: 'RepeatRenamed'
|
471 | port: 'out'
|
472 | graph: 'foo'
|
473 |
|
474 |
|
475 |
|
476 |
|
477 |
|
478 |
|
479 |
|
480 |
|
481 |
|
482 |
|
483 |
|
484 |
|
485 |
|
486 |
|
487 |
|
488 |
|
489 |
|
490 |
|
491 |
|
492 |
|
493 |
|
494 |
|
495 |
|
496 |
|
497 |
|
498 |
|
499 |
|
500 | describe 'removing an out-port of a graph', ->
|
501 | it "should ACK", (done) ->
|
502 | expects = [
|
503 | protocol: 'graph',
|
504 | command: 'removeoutport',
|
505 | payload:
|
506 | graph: 'foo'
|
507 | public: 'out'
|
508 | ]
|
509 | receive expects, done, true
|
510 | send 'graph', 'removeoutport',
|
511 | public: 'out'
|
512 | graph: 'foo'
|
513 |
|
514 |
|
515 |
|
516 |
|
517 |
|
518 |
|
519 |
|
520 | describe 'Network Protocol', ->
|
521 |
|
522 | before (done) ->
|
523 | chai.expect(connection, 'Connection needs to be available').not.be.a 'null'
|
524 | chai.expect(capabilities, 'Network protocol should be allowed for user').to.contain 'protocol:network'
|
525 | waitFor = 5
|
526 | listener = (message) ->
|
527 | waitFor--
|
528 | if waitFor
|
529 | connection.once 'message', listener
|
530 | else
|
531 | done()
|
532 | connection.once 'message', listener
|
533 | send 'graph', 'clear',
|
534 | baseDir: path.resolve __dirname, '../'
|
535 | id: 'bar'
|
536 | main: true
|
537 | send 'graph', 'addnode',
|
538 | id: 'Hello'
|
539 | component: "#{collection}/Repeat"
|
540 | metadata: {}
|
541 | graph: 'bar'
|
542 | send 'graph', 'addnode',
|
543 | id: 'World'
|
544 | component: "#{collection}/Drop"
|
545 | metadata: {}
|
546 | graph: 'bar'
|
547 | send 'graph', 'addedge',
|
548 | src:
|
549 | node: 'Hello'
|
550 | port: 'out'
|
551 | tgt:
|
552 | node: 'World'
|
553 | port: 'in'
|
554 | graph: 'bar'
|
555 | send 'graph', 'addinitial',
|
556 | src:
|
557 | data: 'Hello, world!'
|
558 | tgt:
|
559 | node: 'Hello'
|
560 | port: 'in'
|
561 | graph: 'bar'
|
562 |
|
563 | describe 'on starting the network', ->
|
564 | it 'should process the nodes and stop when it completes', (done) ->
|
565 | expects = [
|
566 | protocol: 'network'
|
567 | command: 'started'
|
568 | payload:
|
569 | graph: 'bar'
|
570 | started: true
|
571 | running: true
|
572 | time: String
|
573 | ,
|
574 | protocol: 'network'
|
575 | command: 'data'
|
576 | payload:
|
577 | id: 'DATA -> IN Hello()'
|
578 | graph: 'bar'
|
579 | tgt: { node: 'Hello', port: 'in' }
|
580 | data: 'Hello, world!'
|
581 | ,
|
582 | protocol: 'network'
|
583 | command: 'data'
|
584 | payload:
|
585 | id: 'Hello() OUT -> IN World()'
|
586 | graph: 'bar'
|
587 | src: { node: 'Hello', port: 'out' }
|
588 | tgt: { node: 'World', port: 'in' }
|
589 | data: 'Hello, world!'
|
590 | ]
|
591 | receive expects, done, true
|
592 | send 'network', 'start',
|
593 | graph: 'bar'
|
594 |
|
595 | it "should provide a 'started' status", (done) ->
|
596 | expects = [
|
597 | protocol: 'network'
|
598 | command: 'status'
|
599 | payload:
|
600 | graph: 'bar'
|
601 | running: false
|
602 | started: true
|
603 | ]
|
604 | receive expects, done, true
|
605 | send 'network', 'getstatus',
|
606 | graph: 'bar'
|
607 | describe 'on stopping the network', ->
|
608 | it 'should be stopped', (done) ->
|
609 | expects = [
|
610 | protocol: 'network'
|
611 | command: 'stopped'
|
612 | payload:
|
613 | graph: 'bar'
|
614 | running: false
|
615 | started: false
|
616 | ]
|
617 | receive expects, done, true
|
618 | send 'network', 'stop',
|
619 | graph: 'bar'
|
620 |
|
621 | it "should provide a 'stopped' status", (done) ->
|
622 | expects = [
|
623 | protocol: 'network'
|
624 | command: 'status'
|
625 | payload:
|
626 | graph: 'bar'
|
627 | running: false
|
628 | started: false
|
629 | ]
|
630 | receive expects, done, true
|
631 | send 'network', 'getstatus',
|
632 | graph: 'bar'
|
633 |
|
634 |
|
635 |
|
636 |
|
637 |
|
638 |
|
639 |
|
640 |
|
641 |
|
642 |
|
643 |
|
644 |
|
645 |
|
646 |
|
647 |
|
648 |
|
649 | describe 'Component Protocol', ->
|
650 | before ->
|
651 | chai.expect(connection, 'Connection needs to be available').not.be.a 'null'
|
652 | chai.expect(capabilities, 'Component protocol should be allowed for connection').to.contain 'protocol:component'
|
653 | describe 'on requesting a component list', ->
|
654 | it 'should receive some known components', (done) ->
|
655 | @timeout 20000
|
656 | listener = (message) ->
|
657 | data = JSON.parse message.utf8Data
|
658 | validateSchema data, '/component/output/list'
|
659 |
|
660 | if data.payload.name is "#{collection}/Repeat"
|
661 | done()
|
662 | else
|
663 | connection.once 'message', listener
|
664 |
|
665 |
|
666 | connection.once 'message', listener
|
667 |
|
668 | send 'component', 'list', {}
|
669 |
|
670 |
|
671 |
|
672 |
|
673 | exports.testRuntimeCommand = (runtimeType, command=null, host='localhost', port=8080, collection='core', version='0.7') ->
|
674 | child = null
|
675 | prefix = ' '
|
676 | exports.testRuntime( runtimeType,
|
677 | (done) ->
|
678 | unless command
|
679 | console.log "#{prefix}not running a command. runtime is assumed to be started"
|
680 | done()
|
681 | return
|
682 | console.log "#{prefix}\"#{command}\" starting"
|
683 | returned = false
|
684 | child = spawn command, [],
|
685 | cwd: process.cwd()
|
686 | shell: true
|
687 | stdio: [
|
688 | 'ignore'
|
689 | 'pipe'
|
690 | 'ignore'
|
691 | ]
|
692 | child.once 'error', (err) ->
|
693 | child = null
|
694 | return if returned
|
695 | returned = true
|
696 | done err
|
697 | child.stdout.once 'data', (data) ->
|
698 | console.log "#{prefix}\"#{command}\" has started"
|
699 | setTimeout ->
|
700 | return if returned
|
701 | returned = true
|
702 | done()
|
703 | , 100
|
704 | child.once 'close', ->
|
705 | console.log "#{prefix}\"#{command}\" exited"
|
706 | child = null
|
707 | return if returned
|
708 | returned = true
|
709 | done new Error 'Child exited'
|
710 | (done) ->
|
711 | return done() unless child
|
712 | child.once 'close', ->
|
713 | done()
|
714 | child.stdout.destroy()
|
715 | child.kill()
|
716 | setTimeout ->
|
717 |
|
718 | return unless child
|
719 | child.kill 'SIGKILL'
|
720 | , 100
|
721 | host
|
722 | port
|
723 | collection
|
724 | version
|
725 | )
|