UNPKG

10.3 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=run.IN:TEST
114 OUTPORT=run.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 }
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 'component:getsource'
204 ]
205 info =
206 type: 'fbp-spec-mocha'
207 version: '0.5'
208 capabilities: capabilities
209 allCapabilities: capabilities
210 graph: 'default/main' # HACK, so Flowhub will ask for our graph
211 runtime.send 'runtime', 'runtime', info, context
212 #sendGraphs mytrace, send, (err) -> # XXX: right place?
213 # ignored
214
215 else if protocol == 'runtime' and command == 'packet'
216 debug 'test message', payload, state.running
217
218 if payload.port != 'test' or payload.event != 'data'
219 debug 'unexpected test message format'
220 return
221
222 state.currentTest = payload.payload
223
224 # collect results of completed tests
225 testDone = (err, test) ->
226 debug 'test completed', test._fbpid, err, Object.keys(state.completedTests).length
227 state.completedTests[test._fbpid] = { test: test, err: err }
228 checkSendCurrentTest()
229
230 checkSendCurrentTest = () ->
231 completed = state.completedTests[state.currentTest]
232 debug 'checking for', state.currentTest, completed?
233 if completed
234 m =
235 graph: state.graph
236 event: 'data'
237 port: 'error'
238 payload: completed.err
239 runtime.send 'runtime', 'packet', m, context
240 delete state.completedTests[state.currentTest]
241
242 checkSendCurrentTest() # we might have completed it already
243
244 if not state.running
245 runTests mocha, testDone, (f) ->
246 updateStatus { running: false }, 'status'
247
248 updateStatus { running: true }, 'status'
249
250 ## Graph
251 else if protocol == 'graph' and command == 'addnode'
252 ackMessage()
253 else if protocol == 'graph' and command == 'addedge'
254 ackMessage()
255 else if protocol == 'graph' and command == 'addinport'
256 ackMessage()
257 else if protocol == 'graph' and command == 'addoutport'
258 ackMessage()
259 else if protocol == 'graph' and command == 'clear'
260 state.graph = payload.id
261 debug 'new graph', state.graph
262 ackMessage()
263
264 ## Network
265 else if protocol == 'network' and command == 'getstatus'
266 runtime.send 'network', 'status', state, context
267
268 else if protocol == 'network' and command == 'start'
269 debug 'FBP network start'
270 updateStatus { started: true, running: false }, 'started'
271 else if protocol == 'network' and command == 'stop'
272 debug 'FBP network stop'
273 updateStatus { started: false, running: false }, 'stopped'
274
275 ## Component
276 else if protocol == 'component' and command == 'list'
277 # one fake component per Mocha suite
278 for s in specs
279 runtime.send 'component', 'component', fbpComponentFromSpec(s), context
280 runtime.send 'component', 'componentsready', {}, context
281
282 else if protocol == 'component' and command == 'getsource'
283 # one fake component per Mocha suite
284 found = null
285 for s in specs
286 componentName = fbpComponentName s
287 if componentName == payload.name
288 found = s
289 debug 'component getsource', "'#{payload.name}'", found?.name
290 if found
291 runtime.send 'component', 'source', fbpSourceFromSpec(found), context
292
293 else if knownUnsupportedCommands protocol, command
294 # ignored
295 else
296 debug 'Warning: Unknown FBP protocol message', protocol, command
297
298## Commandline things
299normalizeOptions = (options) ->
300 if options.host == 'autodetect'
301 options.host = discoverHost()
302 else if match = /autodetect\(([a-z0-9]+)\)/.exec(options.host)
303 options.host = discoverHost(match[1])
304
305 options.port = 3333 if not options.port
306
307 return options
308
309parse = (args) ->
310 program = require 'commander'
311
312 # TODO: take list of files as input instead, to be more mocha compatible
313 program
314 .arguments('<files...>')
315 .action (args) ->
316 program.files = args
317 .option('--host <hostname>', 'Hostname we serve on, for live-url', String, 'autodetect')
318 .option('--port <PORT>', 'Command to launch runtime under test', Number, 3333)
319 .parse(process.argv)
320
321 return program
322
323exports.setup = setup = (options, callback) ->
324 options = normalizeOptions options
325
326 mocha = loadTests options.files
327 specs = buildFbpSpecs mocha
328
329 state =
330 started: false
331 running: false
332 currentTest: null
333 graph: null
334 specs: specs
335 completedTests: {} # 'id' -> { err: ?Error, test: MochaTest }
336 httpServer = new http.Server
337 runtime = websocket httpServer, {}
338 runtime.receive = (protocol, command, payload, context) ->
339 handleFbpCommand state, runtime, mocha, specs, protocol, command, payload, context
340
341 httpServer.listen options.port, (err) ->
342 return callback err, state, httpServer
343
344exports.main = main = () ->
345
346 options = parse process.argv
347
348 setup options, (err, state) ->
349 throw err if err
350 console.log "fbp-spec-mocha started on ws://#{options.host}:#{options.port}"
351 console.log "found #{state.specs.length} test suites"
352
353main() if not module.parent
354