fbp-spec
Version:
Data-driven FBP component/graph testing tool
355 lines (283 loc) • 9.98 kB
text/coffeescript
## Run Mocha testcases using fbp-spec as a runner/frontend
## Intended to allow existing Mocha testcases to be seen and executed
## from a FBP protocol client like Flowhub, without requiring them
## to be rewritten as fbp-spec tests
## This is especially useful to allow partial and gradual migration of existing test suites
## See also ./mocha.coffee, which can be used in combination
# Partially based on example code from https://github.com/mochajs/mocha/wiki/Third-party-UIs
Mocha = require 'mocha'
fs = require 'fs'
path = require 'path'
http = require 'http'
websocket = require './websocket' # FIXME: split out transport interface of noflo-runtime-*, use that directly
debug = require('debug')('fbp-spec:mochacompat')
testsuite = require './testsuite'
loadTests = (files) ->
options = {}
mocha = new Mocha options
for f in files
mocha.addFile f
mocha.loadFiles()
return mocha
# similar to mocha.run(), but files must be loaded beforehand
runTests = (mocha, progress, callback) ->
suite = mocha.suite
options = mocha.options
options.files = mocha.files
runner = new Mocha.Runner suite, options.delay
registerReporter = (r) ->
runner.on 'pass', (test) ->
progress null, test
runner.on 'fail', (test, err) ->
progress err, test
mocha.reporter registerReporter, {}
reporter = new mocha._reporter runner, options
runner.ignoreLeaks = options.ignoreLeaks != false
runner.fullStackTrace = options.fullStackTrace
runner.asyncOnly = options.asyncOnly
runner.allowUncaught = options.allowUncaught
if options.grep
runner.grep options.grep, options.invert
if options.globals
runner.globals options.globals
if options.growl
mocha._growl runner, reporter
if options.useColors?
Mocha.reporters.Base.useColors = options.useColors
Mocha.reporters.Base.inlineDiffs = options.useInlineDiffs
done = (failures) ->
if reporter.done
reporter.done failures, callback
else
callback && callback failures
return runner.run done
testFilesInDirectory = (testDir) ->
files = fs.readdirSync(testDir)
.filter (filename) ->
isJs = filename.substr(-3) == '.js'
isCoffee = filename.substr(-7) == '.coffee'
return isJs or isCoffee
.map (filename) ->
return path.join testDir, filename
return files
testId = (fullname) ->
crypto = require 'crypto'
hash = crypto.createHash 'sha256'
hash.update fullname
return hash.digest('hex').substr(0, 10)
loadSuite = (fbpSuite, suite) ->
for testcase in suite.tests
#console.log 't', testcase
fullName = fbpSuite.name + testcase.parent.title + testcase.title
id = testId fullName
testcase._fbpid = id
fbpCase =
name: testcase.parent.title
assertion: testcase.title
_id: id
inputs:
test: id
expect:
error:
noterror: null
fbpSuite.cases.push fbpCase
# load recursively
for sub in suite.suites
loadSuite fbpSuite, sub
buildFbpSpecs = (mocha) ->
specs = []
top = mocha.suite
for suite in top.suites
#console.log 's', suite
fbpSuite = testsuite.create
name: "#{suite.title} (Mocha tests)"
fixture:
type: 'fbp'
data: """
# @runtime fbp-spec-mocha
INPORT=run.IN:TEST
OUTPORT=run.ERROR:ERROR
runTest(mocha/LoadTestCase) OUT -> IN verifyResult(mocha/CheckResults)
"""
loadSuite fbpSuite, suite
specs.push fbpSuite
return specs
dumpSpecs = (suites) ->
jsyaml = window.jsyaml if window?.jsyaml?
jsyaml = require 'js-yaml' if not jsyaml
str = ""
delimiter = '---\n'
for s in suites
str += "#{jsyaml.safeDump s}"
str += delimiter if suites.length > 1
return str
discoverHost = (preferred_iface) ->
os = require 'os' # node.js only
ifaces = os.networkInterfaces()
address = undefined
int_address = undefined
filter = (connection) ->
if connection.family != 'IPv4'
return
if connection.internal
int_address = connection.address
else
address = connection.address
return
if typeof preferred_iface == 'string' and preferred_iface in ifaces
ifaces[preferred_iface].forEach filter
else
for device of ifaces
ifaces[device].forEach filter
address or int_address
knownUnsupportedCommands = (p, c) ->
return false
fbpComponentName = (s) ->
return "fbp-spec-mocha/#{s.name}" # TODO: use topic/filename?
fbpComponentFromSpec = (s) ->
# component:component
p =
name: fbpComponentName(s)
subgraph: false
inPorts: []
outPorts: []
fbpSourceFromSpec = (s) ->
# component:source message, :getsource response
serialized = dumpSpecs [s]
p =
name: fbpComponentName(s)
code: ''
language: 'whitespace'
tests: serialized
handleFbpCommand = (state, runtime, mocha, specs, protocol, command, payload, context) ->
updateStatus = (news, event) ->
state.started = news.started if news.started?
state.running = news.running if news.running?
debug 'update status', state
runtime.send 'network', event, state, context
#sendEvent = (e) ->
# runtime.send e.protocol, e.command, e.payload, context
ackMessage = ->
# reply with same message as we got in
runtime.send protocol, command, payload, context
## Runtime
if protocol == 'runtime' and command == 'getruntime'
capabilities = [
'protocol:graph' # read-only from client
'protocol:component' # read-only from client
'protocol:network'
'component:getsource'
]
info =
type: 'fbp-spec-mocha'
version: '0.5'
capabilities: capabilities
allCapabilities: capabilities
graph: 'default/main' # HACK, so Flowhub will ask for our graph
runtime.send 'runtime', 'runtime', info, context
#sendGraphs mytrace, send, (err) -> # XXX: right place?
# ignored
else if protocol == 'runtime' and command == 'packet'
debug 'test message', payload, state.running
if payload.port != 'test' or payload.event != 'data'
debug 'unexpected test message format'
return
state.currentTest = payload.payload
if not state.running
testDone = (err, test) ->
debug 'test completed', test._fbpid, state.currentTest, err
if test._fbpid and test._fbpid == state.currentTest
m =
graph: state.graph
event: 'data'
port: 'error'
payload: err
runtime.send 'runtime', 'packet', m, context
runTests mocha, testDone, (f) ->
updateStatus { running: false }, 'status'
updateStatus { running: true }, 'status'
## Graph
else if protocol == 'graph' and command == 'addnode'
ackMessage()
else if protocol == 'graph' and command == 'addedge'
ackMessage()
else if protocol == 'graph' and command == 'addinport'
ackMessage()
else if protocol == 'graph' and command == 'addoutport'
ackMessage()
else if protocol == 'graph' and command == 'clear'
state.graph = payload.id
debug 'new graph', state.graph
ackMessage()
## Network
else if protocol == 'network' and command == 'getstatus'
runtime.send 'network', 'status', state, context
else if protocol == 'network' and command == 'start'
debug 'FBP network start'
updateStatus { started: true, running: false }, 'started'
else if protocol == 'network' and command == 'stop'
debug 'FBP network stop'
updateStatus { started: false, running: false }, 'stopped'
## Component
else if protocol == 'component' and command == 'list'
# one fake component per Mocha suite
for s in specs
runtime.send 'component', 'component', fbpComponentFromSpec(s), context
runtime.send 'component', 'componentsready', {}, context
else if protocol == 'component' and command == 'getsource'
# one fake component per Mocha suite
found = null
for s in specs
componentName = fbpComponentName s
console.log s.name, componentName
if componentName == payload.name
found = s
debug 'component getsource', "'#{payload.name}'", found?.name
if found
runtime.send 'component', 'source', fbpSourceFromSpec(found), context
else if knownUnsupportedCommands protocol, command
# ignored
else
debug 'Warning: Unknown FBP protocol message', protocol, command
## Commandline things
normalizeOptions = (options) ->
if options.host == 'autodetect'
options.host = discoverHost()
else if match = /autodetect\(([a-z0-9]+)\)/.exec(options.host)
options.host = discoverHost(match[1])
return options
parse = (args) ->
program = require 'commander'
# TODO: take list of files as input instead, to be more mocha compatible
program
.arguments('<test directory>')
.action( (dir) -> program.directory = dir )
.option('--ide <URL>', 'FBP IDE to use for live-url', String, 'http://app.flowhub.io')
.option('--host <hostname>', 'Hostname we serve on, for live-url', String, 'autodetect')
.option('--port <PORT>', 'Command to launch runtime under test', Number, 3333)
.parse(process.argv)
return program
exports.setup = setup = (options, callback) ->
options = normalizeOptions options
files = testFilesInDirectory options.directory
mocha = loadTests files
specs = buildFbpSpecs mocha
state =
started: false
running: false
currentTest: null
graph: null
specs: specs
httpServer = new http.Server
runtime = websocket httpServer, {}
runtime.receive = (protocol, command, payload, context) ->
handleFbpCommand state, runtime, mocha, specs, protocol, command, payload, context
httpServer.listen options.port, (err) ->
return callback err, state
exports.main = main = () ->
options = parse process.argv
setup options, (err, state) ->
throw err if err
console.log "fbp-spec-mocha started on ws://#{options.host}:#{options.port}"
console.log "found #{state.specs.length} test suites"
main() if not module.parent