UNPKG

10.4 kBtext/coffeescriptView Raw
1
2## Run Mocha testcases using fbp-spec as a runner/frontend
3## Intended to allow existing Mocha testcases to be seen and executed
4## from a FBP protocol client like Flowhub, without requiring them
5## to be rewritten as fbp-spec tests
6## This is especially useful to allow partial and gradual migration of existing test suites
7## See also ./mocha.coffee, which can be used in combination
8
9# Partially based on example code from https://github.com/mochajs/mocha/wiki/Third-party-UIs
10
11fs = require 'fs'
12path = require 'path'
13http = require 'http'
14websocket = require './websocket' # FIXME: split out transport interface of noflo-runtime-*, use that directly
15Mocha = require 'mocha'
16
17debug = require('debug')('fbp-spec:mochacompat')
18testsuite = require './testsuite'
19
20loadTests = (files) ->
21 options = {}
22 mocha = new Mocha options
23
24 for f in files
25 resolved = require.resolve f
26 delete require.cache[resolved] # clear module cache
27 mocha.addFile f
28 mocha.loadFiles()
29 return mocha
30
31# similar to mocha.run(), but files must be loaded beforehand
32runTests = (mocha, progress, callback) ->
33
34 suite = mocha.suite
35 options = mocha.options
36 options.files = mocha.files
37 runner = new Mocha.Runner suite, options.delay
38 registerReporter = (r) ->
39 runner.on 'pass', (test) ->
40 progress null, test
41 runner.on 'fail', (test, err) ->
42 progress err, test
43
44 mocha.reporter registerReporter, {}
45 reporter = new mocha._reporter runner, options
46
47 runner.ignoreLeaks = options.ignoreLeaks != false
48 runner.fullStackTrace = options.fullStackTrace
49 runner.asyncOnly = options.asyncOnly
50 runner.allowUncaught = options.allowUncaught
51 if options.grep
52 runner.grep options.grep, options.invert
53 if options.globals
54 runner.globals options.globals
55 if options.growl
56 mocha._growl runner, reporter
57 if options.useColors?
58 Mocha.reporters.Base.useColors = options.useColors
59 Mocha.reporters.Base.inlineDiffs = options.useInlineDiffs
60
61 done = (failures) ->
62 if reporter.done
63 reporter.done failures, callback
64 else
65 callback && callback failures
66
67 return runner.run done
68
69testId = (fullname) ->
70 crypto = require 'crypto'
71 hash = crypto.createHash 'sha256'
72 hash.update fullname
73 return hash.digest('hex').substr(0, 10)
74
75loadSuite = (fbpSuite, suite) ->
76
77 for testcase in suite.tests
78 #console.log 't', testcase
79
80 fullName = fbpSuite.name + testcase.parent.title + testcase.title # FIXME: make the full name recursive
81 id = testId fullName # FIXME: salt so it does not collide
82 testcase._fbpid = id
83 fbpCase =
84 name: testcase.parent.title
85 assertion: testcase.title
86 _id: id
87 inputs:
88 test: id
89 expect:
90 error:
91 noterror: null
92
93 fbpSuite.cases.push fbpCase
94
95 # load recursively
96 for sub in suite.suites
97 loadSuite fbpSuite, sub
98
99buildFbpSpecs = (mocha) ->
100 specs = []
101
102 top = mocha.suite
103 for suite in top.suites
104 #console.log 's', suite
105
106 fbpSuite = testsuite.create
107 name: "#{suite.title} (Mocha tests)"
108 fixture:
109 type: 'fbp'
110 data: """
111 # @runtime fbp-spec-mocha
112
113 INPORT=runTest.IN:TEST
114 OUTPORT=runTest.ERROR:ERROR
115
116 runTest(mocha/LoadTestCase) OUT -> IN verifyResult(mocha/CheckResults)
117 """
118
119 loadSuite fbpSuite, suite
120 specs.push fbpSuite
121
122 return specs
123
124dumpSpecs = (suites) ->
125 jsyaml = window.jsyaml if window?.jsyaml?
126 jsyaml = require 'js-yaml' if not jsyaml
127
128 str = ""
129 delimiter = '---\n'
130 for s in suites
131 str += "#{jsyaml.safeDump s}"
132 str += delimiter if suites.length > 1
133
134 return str
135
136discoverHost = (preferred_iface) ->
137 os = require 'os' # node.js only
138
139 ifaces = os.networkInterfaces()
140 address = undefined
141 int_address = undefined
142
143 filter = (connection) ->
144 if connection.family != 'IPv4'
145 return
146 if connection.internal
147 int_address = connection.address
148 else
149 address = connection.address
150 return
151
152 if typeof preferred_iface == 'string' and preferred_iface in ifaces
153 ifaces[preferred_iface].forEach filter
154 else
155 for device of ifaces
156 ifaces[device].forEach filter
157 address or int_address
158
159knownUnsupportedCommands = (p, c) ->
160 return false
161
162fbpComponentName = (s) ->
163 return "fbp-spec-mocha/#{s.name}" # TODO: use topic/filename?
164
165fbpComponentFromSpec = (s) ->
166 # component:component
167 p =
168 name: fbpComponentName(s)
169 subgraph: false
170 inPorts: []
171 outPorts: []
172
173fbpSourceFromSpec = (s) ->
174 # component:source message, :getsource response
175 serialized = dumpSpecs [s]
176 p =
177 name: fbpComponentName(s)
178 code: ''
179 language: 'whitespace'
180 tests: serialized
181
182handleFbpCommand = (state, runtime, mocha, specs, protocol, command, payload, context) ->
183
184 updateStatus = (news, event) ->
185 state.started = news.started if news.started?
186 state.running = news.running if news.running?
187 runtimeState = { started: state.started, running: state.running, graph: payload.graph }
188 debug 'update status', runtimeState
189 runtime.send 'network', event, runtimeState, context
190
191 #sendEvent = (e) ->
192 # runtime.send e.protocol, e.command, e.payload, context
193 ackMessage = ->
194 # reply with same message as we got in
195 runtime.send protocol, command, payload, context
196
197 ## Runtime
198 if protocol == 'runtime' and command == 'getruntime'
199 capabilities = [
200 'protocol:graph' # read-only from client
201 'protocol:component' # read-only from client
202 'protocol:network'
203 'protocol:runtime'
204 'component:getsource'
205 ]
206 info =
207 type: 'fbp-spec-mocha'
208 version: '0.5'
209 capabilities: capabilities
210 allCapabilities: capabilities
211 graph: 'default/main' # HACK, so Flowhub will ask for our graph
212 runtime.send 'runtime', 'runtime', info, context
213 #sendGraphs mytrace, send, (err) -> # XXX: right place?
214 # ignored
215
216 else if protocol == 'runtime' and command == 'packet'
217 debug 'test message', payload, state.running
218
219 if payload.port != 'test' or payload.event != 'data'
220 debug 'unexpected test message format'
221 return
222
223 state.currentTest = payload.payload
224
225 # collect results of completed tests
226 testDone = (err, test) ->
227 debug 'test completed', test._fbpid, err, Object.keys(state.completedTests).length
228 state.completedTests[test._fbpid] = { test: test, err: err }
229 checkSendCurrentTest()
230
231 checkSendCurrentTest = () ->
232 completed = state.completedTests[state.currentTest]
233 debug 'checking for', state.currentTest, completed?
234 if completed
235 m =
236 graph: state.graph
237 event: 'data'
238 port: 'error'
239 payload: completed.err
240 runtime.send 'runtime', 'packet', m, context
241 delete state.completedTests[state.currentTest]
242
243 checkSendCurrentTest() # we might have completed it already
244
245 if not state.running
246 runTests mocha, testDone, (f) ->
247 updateStatus { running: false }, 'status'
248
249 updateStatus { running: true }, 'status'
250
251 ## Graph
252 else if protocol == 'graph' and command == 'addnode'
253 ackMessage()
254 else if protocol == 'graph' and command == 'addedge'
255 ackMessage()
256 else if protocol == 'graph' and command == 'addinport'
257 ackMessage()
258 else if protocol == 'graph' and command == 'addoutport'
259 ackMessage()
260 else if protocol == 'graph' and command == 'clear'
261 state.graph = payload.id
262 debug 'new graph', state.graph
263 ackMessage()
264
265 ## Network
266 else if protocol == 'network' and command == 'getstatus'
267 runtime.send 'network', 'status', state, context
268
269 else if protocol == 'network' and command == 'start'
270 debug 'FBP network start'
271 updateStatus { started: true, running: false, graph: payload.graph }, 'started'
272 else if protocol == 'network' and command == 'stop'
273 debug 'FBP network stop'
274 updateStatus { started: false, running: false, graph: payload.graph }, 'stopped'
275
276 ## Component
277 else if protocol == 'component' and command == 'list'
278 # one fake component per Mocha suite
279 for s in specs
280 runtime.send 'component', 'component', fbpComponentFromSpec(s), context
281 runtime.send 'component', 'componentsready', specs.length, context
282
283 else if protocol == 'component' and command == 'getsource'
284 # one fake component per Mocha suite
285 found = null
286 for s in specs
287 componentName = fbpComponentName s
288 if componentName == payload.name
289 found = s
290 debug 'component getsource', "'#{payload.name}'", found?.name
291 if found
292 runtime.send 'component', 'source', fbpSourceFromSpec(found), context
293
294 else if knownUnsupportedCommands protocol, command
295 # ignored
296 else
297 debug 'Warning: Unknown FBP protocol message', protocol, command
298
299## Commandline things
300normalizeOptions = (options) ->
301 if options.host == 'autodetect'
302 options.host = discoverHost()
303 else if match = /autodetect\(([a-z0-9]+)\)/.exec(options.host)
304 options.host = discoverHost(match[1])
305
306 options.port = 3333 if not options.port
307
308 return options
309
310parse = (args) ->
311 program = require 'commander'
312
313 # TODO: take list of files as input instead, to be more mocha compatible
314 program
315 .arguments('<files...>')
316 .action (args) ->
317 program.files = args
318 .option('--host <hostname>', 'Hostname we serve on, for live-url', String, 'autodetect')
319 .option('--port <PORT>', 'Command to launch runtime under test', Number, 3333)
320 .parse(process.argv)
321
322 return program
323
324exports.setup = setup = (options, callback) ->
325 options = normalizeOptions options
326
327 mocha = loadTests options.files
328 specs = buildFbpSpecs mocha
329
330 state =
331 started: false
332 running: false
333 currentTest: null
334 graph: null
335 specs: specs
336 completedTests: {} # 'id' -> { err: ?Error, test: MochaTest }
337 httpServer = new http.Server
338 runtime = websocket httpServer, {}
339 runtime.receive = (protocol, command, payload, context) ->
340 handleFbpCommand state, runtime, mocha, specs, protocol, command, payload, context
341
342 httpServer.listen options.port, (err) ->
343 return callback err, state, httpServer
344
345exports.main = main = () ->
346
347 options = parse process.argv
348
349 setup options, (err, state) ->
350 throw err if err
351 console.log "fbp-spec-mocha started on ws://#{options.host}:#{options.port}"
352 console.log "found #{state.specs.length} test suites"
353
354main() if not module.parent
355