1 | common = require './common'
|
2 | protocol = require './protocol'
|
3 | testsuite = require './testsuite'
|
4 | expectation = require './expectation'
|
5 |
|
6 | fbp = require 'fbp'
|
7 | fbpClient = require 'fbp-client'
|
8 | debug = require('debug')('fbp-spec:runner')
|
9 | Promise = require 'bluebird'
|
10 |
|
11 | debugReceivedMessages = (client) ->
|
12 | debugReceived = require('debug')('fbp-spec:runner:received')
|
13 | client.on 'signal', ({protocol, command, payload}) ->
|
14 | debugReceived protocol, command, payload
|
15 |
|
16 | synthesizeTopicFixture = (topic, components, callback) ->
|
17 | debug 'synthesizing fixture', topic
|
18 |
|
19 |
|
20 | component = components[topic]
|
21 | return callback new Error "Could not find component for topic: #{topic}" if not component
|
22 |
|
23 | graph =
|
24 | properties: {}
|
25 | inports: {}
|
26 | outports: {}
|
27 | processes: {}
|
28 | connections: []
|
29 |
|
30 | processName = 'testee'
|
31 | graph.processes[processName] =
|
32 | component: topic
|
33 |
|
34 | for port in component.inPorts
|
35 | portName = port.id
|
36 | graph.inports[portName] =
|
37 | process: processName
|
38 | port: portName
|
39 | for port in component.outPorts
|
40 | portName = port.id
|
41 | graph.outports[portName] =
|
42 | process: processName
|
43 | port: portName
|
44 |
|
45 |
|
46 | if Object.keys(graph.outports) < 1
|
47 | return callback new Error "Component '#{topic}' used as fixture has no outports"
|
48 | if Object.keys(graph.inports) < 1
|
49 | return callback new Error "Component '#{topic}' used as fixture has no inports"
|
50 |
|
51 | return callback null, graph
|
52 |
|
53 |
|
54 |
|
55 | getFixtureGraph = (context, suite, callback) ->
|
56 |
|
57 |
|
58 | hasComponents = (s) ->
|
59 | return s.components? and typeof s.components == 'object' and Object.keys(s.components).length
|
60 |
|
61 | updateComponents = (cb) ->
|
62 | return cb null if hasComponents context
|
63 | protocol.getComponents context.client, (err, components) ->
|
64 | return cb err if err
|
65 | context.components = components
|
66 | return cb null
|
67 | return
|
68 |
|
69 | updateComponents (err) ->
|
70 | return callback err if err
|
71 |
|
72 | if not suite.fixture?
|
73 | return synthesizeTopicFixture suite.topic, context.components, callback
|
74 | else if suite.fixture.type == 'json'
|
75 | try
|
76 | graph = JSON.parse suite.fixture.data
|
77 | catch e
|
78 | return callback e
|
79 | return callback null, graph
|
80 | else if suite.fixture.type == 'fbp'
|
81 | try
|
82 | graph = fbp.parse suite.fixture.data
|
83 | catch e
|
84 | return callback e
|
85 |
|
86 | graph.properties = {} if not graph.properties
|
87 | return callback null, graph
|
88 | else
|
89 | return callback new Error "Unknown fixture type #{suite.fixture.type}"
|
90 | return
|
91 |
|
92 | sendMessageAndWait = (client, currentGraph, inputData, expectData, callback) ->
|
93 | observer = client.observe (s) -> s.protocol is 'runtime' and s.command is 'packet' and s.payload.graph is currentGraph
|
94 |
|
95 | signalsToReceived = (signals) ->
|
96 | received = {}
|
97 | for signal in signals
|
98 | received[signal.payload.port] = signal.payload.payload
|
99 | return received
|
100 |
|
101 | checkSuccess = (s) ->
|
102 |
|
103 | return false unless s.protocol is 'runtime' and s.command is 'packet'
|
104 |
|
105 | return false unless s.payload.graph is currentGraph
|
106 |
|
107 | return false unless s.payload.event is 'data'
|
108 |
|
109 | receivedSignals = observer.signals.slice 0, observer.signals.indexOf(s) + 1
|
110 | received = signalsToReceived receivedSignals
|
111 | result = (Object.keys(received).length == Object.keys(expectData).length)
|
112 | return result
|
113 | checkFailure = (s) ->
|
114 | if s.protocol is 'network' and s.command is 'error'
|
115 |
|
116 | return false if s.payload.graph and s.payload.graph isnt currentGraph
|
117 | return true
|
118 | if s.protocol is 'network' and s.command is 'processerror'
|
119 |
|
120 | return false if s.payload.graph and s.payload.graph isnt currentGraph
|
121 | return true
|
122 | if s.protocol is 'runtime' and s.command is 'packet'
|
123 |
|
124 |
|
125 | return false unless s.payload.graph is currentGraph
|
126 |
|
127 | return false unless s.payload.event is 'data'
|
128 |
|
129 | return false unless s.payload.port is 'error'
|
130 |
|
131 | return false unless typeof expectData.error is 'undefined'
|
132 | return true
|
133 |
|
134 | false
|
135 |
|
136 |
|
137 | sendPackets = Promise.promisify protocol.sendPackets
|
138 | Promise.resolve()
|
139 | .then(() -> sendPackets client, currentGraph, inputData)
|
140 | .then(() -> observer.until(checkSuccess, checkFailure))
|
141 | .then((signals) -> signalsToReceived(signals))
|
142 | .nodeify(callback)
|
143 | return
|
144 |
|
145 | needsSetup = (suite) ->
|
146 | notSkipped = suite.cases.filter((c) -> not c.skip)
|
147 | return notSkipped.length > 0
|
148 |
|
149 | class Runner
|
150 | constructor: (@client, options={}) ->
|
151 | @currentGraphId = null
|
152 | @components = {}
|
153 | @parentElement = null
|
154 | @options = options
|
155 | @options.connectTimeout = 5*1000 if not @options.connectTimeout?
|
156 | @options.commandTimeout = 3*1000 if not @options.commandTimeout?
|
157 |
|
158 | prepareClient: (callback) ->
|
159 | if @client.protocol? and @client.address?
|
160 |
|
161 | Promise.resolve()
|
162 | .then(() => fbpClient(@client, { commandTimeout: @options.commandTimeout }))
|
163 | .then((client) =>
|
164 | @client = client
|
165 |
|
166 | if @parentElement and client.definition.protocol is 'iframe'
|
167 |
|
168 | client.transport.setParentElement @parentElement
|
169 |
|
170 | return client
|
171 | )
|
172 | .nodeify(callback)
|
173 | return
|
174 |
|
175 | callback null, @client
|
176 | return
|
177 |
|
178 |
|
179 | connect: (callback) ->
|
180 | debug 'connect'
|
181 |
|
182 | @prepareClient (err) =>
|
183 | return callback err if err
|
184 |
|
185 | debugReceivedMessages @client
|
186 | @client.on 'network', ({command, payload}) ->
|
187 | console.log payload.message if command is 'output' and payload.message
|
188 |
|
189 | timeBetweenAttempts = 500
|
190 | attempts = Math.floor(@options.connectTimeout / timeBetweenAttempts)
|
191 | isOnline = () =>
|
192 | connected = @client.isConnected()
|
193 | return if connected then Promise.resolve() else Promise.reject new Error 'Not connected to runtime'
|
194 | tryConnect = () =>
|
195 | debug 'trying to connect'
|
196 | return @client.connect()
|
197 | common.retryUntil(tryConnect, isOnline, timeBetweenAttempts, attempts)
|
198 | .then(() => @checkCapabilities(['protocol:graph', 'protocol:network', 'protocol:runtime']))
|
199 | .nodeify(callback)
|
200 | return
|
201 |
|
202 | disconnect: (callback) ->
|
203 | debug 'disconnect'
|
204 |
|
205 | return callback() unless @client?.isConnected()
|
206 |
|
207 | Promise.resolve()
|
208 | .then(() => @client.disconnect())
|
209 | .nodeify(callback)
|
210 | return
|
211 |
|
212 | checkCapabilities: (capabilities) ->
|
213 | unless @client.isConnected()
|
214 | return Promise.reject new Error 'Not connected to runtime'
|
215 | unless @client.definition?.capabilities?.length
|
216 | return Promise.reject new Error 'Runtime provides no capabilities'
|
217 | for capability in capabilities
|
218 | if @client.definition.capabilities.indexOf(capability) is -1
|
219 | return Promise.reject new Error "Runtime doesn't provide #{capability}"
|
220 | return Promise.resolve()
|
221 |
|
222 | setupSuite: (suite, callback) ->
|
223 | debug 'setup suite', "\"#{suite.name}\""
|
224 | return callback null if not needsSetup suite
|
225 |
|
226 | unless @client.isConnected()
|
227 | return callback new Error 'Disconnected from runtime'
|
228 |
|
229 | getFixtureGraph this, suite, (err, graph) =>
|
230 | return callback err if err
|
231 | protocol.sendGraph @client, graph, (err, graphId) =>
|
232 | @currentGraphId = graphId
|
233 | return callback err if err
|
234 | protocol.startNetwork @client, graphId, callback
|
235 | return
|
236 | return
|
237 |
|
238 | teardownSuite: (suite, callback) ->
|
239 | debug 'teardown suite', "\"#{suite.name}\""
|
240 | return callback null if not needsSetup suite
|
241 |
|
242 | unless @client.isConnected()
|
243 | return callback new Error 'Disconnected from runtime'
|
244 |
|
245 |
|
246 | protocol.stopNetwork @client, @currentGraphId, callback
|
247 | return
|
248 |
|
249 | runTest: (testcase, callback) ->
|
250 | debug 'runtest', "\"#{testcase.name}\""
|
251 |
|
252 | unless @client.isConnected()
|
253 | return callback new Error 'Disconnected from runtime'
|
254 |
|
255 |
|
256 | inputs = if common.isArray(testcase.inputs) then testcase.inputs else [ testcase.inputs ]
|
257 | expects = if common.isArray(testcase.expect) then testcase.expect else [ testcase.expect ]
|
258 | sequence = []
|
259 | for i in [0...inputs.length]
|
260 | sequence.push
|
261 | inputs: inputs[i]
|
262 | expect: expects[i]
|
263 |
|
264 | sendWait = (data, cb) =>
|
265 | sendMessageAndWait @client, @currentGraphId, data.inputs, data.expect, cb
|
266 | common.asyncSeries sequence, sendWait, (err, actuals) ->
|
267 | return callback err if err
|
268 | actuals.forEach (r, idx) ->
|
269 | sequence[idx].actual = r
|
270 | return callback null, sequence
|
271 | return
|
272 |
|
273 |
|
274 | checkResults = (results) ->
|
275 | actuals = results.filter (r) -> r.actual?
|
276 | expects = results.filter (r) -> r.expect?
|
277 | if actuals.length < expects.length
|
278 | result =
|
279 | passed: false
|
280 | error: new Error "Only got #{actual.length} output messages out of #{expect.length}"
|
281 | return result
|
282 |
|
283 | results = results.map (res) ->
|
284 | res.error = null
|
285 | try
|
286 | expectation.expect res.expect, res.actual
|
287 | catch e
|
288 |
|
289 | res.error = e
|
290 | return res
|
291 |
|
292 | failures = results.filter (r) -> r.error
|
293 | if failures.length == 0
|
294 | result =
|
295 | passed: true
|
296 | else
|
297 | if expects.length == 1
|
298 | result =
|
299 | error: failures[0].error
|
300 | else if expects.length > 1 and failures.length == 1
|
301 | index = results.findIndex (r) -> r.error
|
302 | failed = results[index]
|
303 | result =
|
304 | error: new Error "Expectation #{index} of sequence failed: #{failed.error.message}"
|
305 | else
|
306 | errors = results.map (r) -> r.error?.message or ''
|
307 | result =
|
308 | error: new Error "Multiple failures in sequence: #{errors}"
|
309 |
|
310 | return result
|
311 |
|
312 | runTestAndCheck = (runner, testcase, callback) ->
|
313 | return callback null, { passed: true } if testcase.skip
|
314 |
|
315 |
|
316 |
|
317 | inputs = if common.isArray(testcase.inputs) then testcase.inputs else [ testcase.inputs ]
|
318 | expects = if common.isArray(testcase.expect) then testcase.expect else [ testcase.expect ]
|
319 | if inputs.length != expects.length
|
320 | return callback null,
|
321 | passed: false
|
322 | error: new Error "Test sequence length mismatch. Got #{inputs.length} inputs and #{expects.length} expectations"
|
323 |
|
324 | runner.runTest testcase, (err, results) ->
|
325 | if err
|
326 |
|
327 | result =
|
328 | passed: false
|
329 | error: err
|
330 | return callback null, result
|
331 | result = checkResults results
|
332 | if result.error
|
333 | result.passed = false
|
334 | return callback null, result
|
335 | return
|
336 |
|
337 | runSuite = (runner, suite, runTest, callback) ->
|
338 | return callback null, suite if suite.skip
|
339 |
|
340 |
|
341 | runner.setupSuite suite, (err) ->
|
342 | debug 'setup suite', err
|
343 | return callback err, suite if err
|
344 |
|
345 | common.asyncSeries suite.cases, runTest, (err) ->
|
346 | debug 'testrun complete', err
|
347 |
|
348 | runner.teardownSuite suite, (err) ->
|
349 | debug 'teardown suite', err
|
350 | return callback err, suite
|
351 | return
|
352 |
|
353 |
|
354 | exports.getComponentSuites = (runner, callback) ->
|
355 | protocol.getCapabilities runner.client, (err, caps) ->
|
356 | return callback err if err
|
357 | return callback null, [] unless 'component:getsource' in caps
|
358 |
|
359 | protocol.getComponentTests runner.client, (err, tests) ->
|
360 | return callback err if err
|
361 | suites = loadComponentSuites tests
|
362 | debug 'get component suites', tests.length, suites.length
|
363 | return callback null, suites
|
364 | return
|
365 |
|
366 | loadComponentSuites = (componentTests) ->
|
367 | suites = []
|
368 | for name, tests of componentTests
|
369 | try
|
370 | ss = testsuite.loadYAML tests
|
371 | suites = suites.concat ss
|
372 | catch e
|
373 |
|
374 |
|
375 | continue
|
376 | return suites
|
377 |
|
378 |
|
379 |
|
380 | runAll = (runner, suites, updateCallback, doneCallback) ->
|
381 |
|
382 | runTest = (testcase, callback) ->
|
383 | done = (error) ->
|
384 | updateCallback suites
|
385 | callback error
|
386 |
|
387 | runTestAndCheck runner, testcase, (err, results) ->
|
388 | for key, val of results
|
389 | testcase[key] = val
|
390 | testcase.error = testcase.error.message if testcase.error
|
391 | debug 'ran test', '"testcase.name"', testcase.passed, err
|
392 | return done null
|
393 |
|
394 | runOneSuite = (suite, cb) ->
|
395 | runSuite runner, suite, runTest, cb
|
396 |
|
397 | debug 'running suites', (s.name for s in suites)
|
398 | common.asyncSeries suites, runOneSuite, (err) ->
|
399 | return doneCallback err
|
400 |
|
401 | return
|
402 |
|
403 | exports.Runner = Runner
|
404 | exports.runAll = runAll
|
405 | exports.runTestAndCheck = runTestAndCheck
|