UNPKG

23.2 kBtext/coffeescriptView Raw
1chai = require 'chai'
2path = require 'path'
3{ spawn } = require 'child_process'
4WebSocketClient = require('websocket').client
5semver = require 'semver'
6tv4 = require '../schema/index.js'
7
8validateSchema = (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
14exports.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 # FIXME: Remove from payload once runtimes are on 0.8
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 # Basic validation
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 # FIXME: Remove once runtimes are on 0.8
86 delete data.payload.secret
87
88 # Validate all received packets against schema
89 validateSchema data, getPacketSchema data
90 # Don't ever expect payloads to return a secret
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 # We received an unexpected error, bail out
96 done new Error data.payload
97 return
98
99 if allowExtraPackets and not messageMatches data, expects[0]
100 # Ignore messages we don't care about in context of the test
101 connection.once 'message', listener
102 return
103
104 # Validate actual payload
105 expected = expects.shift()
106 chai.expect(getPacketSchema(data)).to.equal getPacketSchema expected
107
108 # Clear values that can't be checked with deep equal. We already
109 # know they passed schema validation
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 # FIXME: Remove once runtimes are on 0.8
126 delete expected.payload.secret
127 # Don't ever expect payloads to return a secret
128 delete expected.secret
129
130 chai.expect(data.payload).to.eql expected.payload
131 # Received all expected packets
132 return done() unless expects.length
133 # Still waiting
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 # describe 'adding an edge to a non-existent node', ->
212 # it 'should return an error', (done) ->
213 # expects = [
214 # protocol: 'graph'
215 # command: 'error'
216 # payload:
217 # msg: 'Requested port not found'
218 # ]
219 # receive expects, done
220 # send 'graph', 'addedge',
221 # protocol: 'graph'
222 # command: 'addedge'
223 # payload:
224 # src:
225 # node: 'non-existent'
226 # port: 'out'
227 # tgt:
228 # node: 'Drop1'
229 # port: 'in'
230 # graph: 'foo'
231 # describe 'adding an edge to a non-existent port', ->
232 # it 'should return an error', (done) ->
233 # expects = [
234 # protocol: 'graph'
235 # command: 'error'
236 # payload:
237 # msg: 'Requested port not found'
238 # ]
239 # receive expects, done
240 # send 'graph', 'addedge',
241 # protocol: 'graph'
242 # command: 'addedge'
243 # payload:
244 # src:
245 # node: 'Repeat1'
246 # port: 'non-existent'
247 # tgt:
248 # node: 'Drop1'
249 # port: 'in'
250 # graph: 'foo'
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 # Runtimes can send 'changeedge' and 'changenode' as part
359 # of the response for better fbp-graph/Journal behavior
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 # describe 'adding an in-port to a non-existent port', ->
443 # it "should return an error", (done) ->
444 # expects = [
445 # protocol: 'graph',
446 # command: 'error',
447 # payload:
448 # msg: 'Requested port not found'
449 # ]
450 # receive expects, done
451 # send 'graph', 'addinport',
452 # public: 'in'
453 # node: 'non-existent'
454 # port: 'in'
455 # graph: 'foo'
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 # describe 'renaming an in-port of a graph', ->
474 # it "should provide the graph's ports back", (done) ->
475 # expects = [
476 # protocol: 'runtime',
477 # command: 'ports',
478 # payload:
479 # graph: 'foo'
480 # inPorts:
481 # [
482 # addressable: false
483 # id: "input"
484 # required: false
485 # type: "any"
486 # ]
487 # outPorts:
488 # [
489 # addressable: false
490 # id: "out"
491 # required: false
492 # type: "any"
493 # ]
494 # ]
495 # receive expects, done
496 # send 'graph', 'renameinport',
497 # from: 'in'
498 # to: 'input'
499 # graph: 'foo'
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 # TODO:
514 # ports:
515 # removeinport
516 # renameoutport
517 # groups:
518 # addgroup / removegroup / renamegroup / changegroup
519
520 describe 'Network Protocol', ->
521 # Set up a clean graph
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 # set this to the number of commands below
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 # describe 'on console output', ->
634 # it 'should be able to capture and transmit it', (done) ->
635 # listener = (message) ->
636 # check done, ->
637 # rt.stopCapture()
638 # chai.expect(message.utf8Data).to.be.a 'string'
639 # msg = JSON.parse message.utf8Data
640 # chai.expect(msg.protocol).to.equal 'network'
641 # chai.expect(msg.command).to.equal 'output'
642 # chai.expect(msg.payload).to.be.an 'object'
643 # chai.expect(msg.payload.message).to.equal 'Hello, World!'
644 # done()
645 # connection.once 'message', listener
646 # rt.startCapture()
647 # console.log 'Hello, World!'
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 # TODO:
671 # getsource => source
672
673exports.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 # If SIGTERM didn't do it, try harder
718 return unless child
719 child.kill 'SIGKILL'
720 , 100
721 host
722 port
723 collection
724 version
725 )