UNPKG

13.7 kBtext/coffeescriptView Raw
1common = require './common'
2protocol = require './protocol'
3testsuite = require './testsuite'
4expectation = require './expectation'
5
6fbp = require 'fbp'
7fbpClient = require 'fbp-client'
8debug = require('debug')('fbp-spec:runner')
9Promise = require 'bluebird'
10
11debugReceivedMessages = (client) ->
12 debugReceived = require('debug')('fbp-spec:runner:received')
13 client.on 'signal', ({protocol, command, payload}) ->
14 debugReceived protocol, command, payload
15
16synthesizeTopicFixture = (topic, components, callback) ->
17 debug 'synthesizing fixture', topic
18 # export each of the ports for topic component
19 # return a FBP graph?
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 # Sanity checking if this is usable as a fixture
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# @context should have .components = {} property
55getFixtureGraph = (context, suite, callback) ->
56 # TODO: follow runtime for component changes
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
92sendMessageAndWait = (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 # We're only interested in response packets
103 return false unless s.protocol is 'runtime' and s.command is 'packet'
104 # Check that is for the current graph under test
105 return false unless s.payload.graph is currentGraph
106 # We're only interested in data IPs, not brackets
107 return false unless s.payload.event is 'data'
108 # Get signals received until this message
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 # network:error should imply failed test
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 # network:processerror should imply failed test
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 # Output packet, see if it is an unexpected error
124 # Check that is for the current graph under test
125 return false unless s.payload.graph is currentGraph
126 # We're only interested in data IPs, not brackets
127 return false unless s.payload.event is 'data'
128 # We only care for packets to error port
129 return false unless s.payload.port is 'error'
130 # We only care if spec isn't expecting errors
131 return false unless typeof expectData.error is 'undefined'
132 return true
133
134 false
135
136 # send input packets
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
145needsSetup = (suite) ->
146 notSkipped = suite.cases.filter((c) -> not c.skip)
147 return notSkipped.length > 0
148
149class 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 # is a runtime definition
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 # We need to set up the parent element in this case
168 client.transport.setParentElement @parentElement
169
170 return client
171 )
172 .nodeify(callback)
173 return
174 # This is a client instance
175 callback null, @client
176 return
177
178 # TODO: check the runtime capabilities before continuing
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 # FIXME: also remove the graph. Ideally using a 'destroy' message in FBP protocol
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 # XXX: normalize and validate in testsuite.coffee instead?
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# TODO: should this go into expectation?
274checkResults = (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 # FIXME: only catch actual AssertionErrors
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
312runTestAndCheck = (runner, testcase, callback) ->
313 return callback null, { passed: true } if testcase.skip
314 # TODO: pass some skipped state? its indirectly in .skip though
315
316 # XXX: normalize and validate in testsuite.coffee instead?
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 # Map error to a test failure
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
337runSuite = (runner, suite, runTest, callback) ->
338 return callback null, suite if suite.skip
339 # TODO: pass some skipped state? its indirectly in .skip though
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
354exports.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
366loadComponentSuites = (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 # ignore, could be non fbp-spec test
374 # TODO: include tests type in FBP protocol, so we know whether this is error or legit
375 continue
376 return suites
377
378# will update each of the testcases in @suites
379# with .passed and .error states as tests are ran
380runAll = (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 # ignore error to not bail out early
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
403exports.Runner = Runner
404exports.runAll = runAll
405exports.runTestAndCheck = runTestAndCheck