UNPKG

fbp-spec

Version:

Data-driven FBP component/graph testing tool

355 lines (283 loc) 9.98 kB
## 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