UNPKG

noflo-nodejs

Version:

Command-line tool for running NoFlo programs on Node.js

380 lines (376 loc) 12.6 kB
const { exec, spawn } = require('child_process'); const { expect } = require('chai'); const { promisify } = require('util'); const { v4: uuid } = require('uuid'); const path = require('path'); const fs = require('fs'); const fbpHealthCheck = require('fbp-protocol-healthcheck'); const fbpClient = require('fbp-client'); const fbpGraph = require('fbp-graph'); function healthCheck(address, callback) { fbpHealthCheck(address) .then(() => callback(), () => healthCheck(address, callback)); } function waitFor(time) { return new Promise((resolve) => { setTimeout(resolve, time); }); } describe('noflo-nodejs CLI', () => { const prog = path.resolve(__dirname, '../bin/noflo-nodejs'); const runtimeSecret = process.env.FBP_PROTOCOL_SECRET || 'noflo-nodejs'; describe('--graph=helloworld.fbp --batch --trace', () => { let stdout = ''; let stderr = ''; const graph = path.resolve(__dirname, './fixtures/helloworld.fbp'); it('should execute graph and exit', (done) => { const cmd = `${prog} --graph=${graph} --batch --trace --open=false`; exec(cmd, (err, o, e) => { if (err) { done(err); return; } stdout = o; stderr = e; done(); }); }).timeout(10 * 1000); it('should have written the expected output', () => { expect(stdout).to.contain('hello world'); }); it('should not have written any errors', () => { expect(stderr).to.eql(''); }); it('should have produced a flowtrace', () => { expect(stdout.toLowerCase()).to.include('wrote flowtrace to:'); }); }); describe('--graph=missingcomponent.fbp', () => { const graph = path.resolve(__dirname, './fixtures/missingcomponent.fbp'); it('should fail with an error telling about the missing component', (done) => { const cmd = `${prog} --graph=${graph} --open=false`; exec(cmd, (err) => { expect(err.message).to.contain('Component foo/Bar not available'); done(); }); }).timeout(10 * 1000); }); describe('--graph=helloin.fbp', () => { const baseDir = path.resolve(__dirname, './fixtures/graph-as-component'); const graph = path.resolve(baseDir, './graphs/helloin.fbp'); let runtimeProcess; let runtimeClient; before('start runtime', (done) => { runtimeProcess = spawn(prog, [ '--host=localhost', '--port=3470', '--open=false', `--secret=${runtimeSecret}`, `--base-dir=${baseDir}`, `--graph=${graph}`, ]); runtimeProcess.stdout.pipe(process.stdout); runtimeProcess.stderr.pipe(process.stderr); healthCheck('ws://localhost:3470', done); }); after('stop runtime', (done) => { if (!runtimeProcess) { done(); return; } process.kill(runtimeProcess.pid); done(); }); it('should be possible to connect', () => fbpClient({ address: 'ws://localhost:3470', protocol: 'websocket', secret: runtimeSecret, }) .then((c) => { runtimeClient = c; return c.connect(); })); it('should have marked the graph as the main', () => { expect(runtimeClient.definition.graph).to.equal('graph-as-component/HelloIn'); }); it('should be possible to get graph sources', () => runtimeClient .protocol.component.getsource({ name: 'graph-as-component/HelloIn', })); it('should be possible to get the component list', () => runtimeClient .protocol.component.list() .then((components) => { const expectedNames = [ 'graph-as-component/Repeat', 'graph-as-component/Output', 'Graph', 'graph-as-component/HelloIn', ]; const names = components.map((c) => c.name); names.sort(); expectedNames.sort(); expect(names).to.eql(expectedNames); })); it('should be possible to get status of the running network', () => runtimeClient .protocol.network.getstatus({ graph: 'graph-as-component/HelloIn', })); }); describe('--auto-save', () => { const baseDir = path.resolve(__dirname, './fixtures/auto-save'); const readFile = promisify(fs.readFile); const unlink = promisify(fs.unlink); let runtimeProcess; let runtimeClient; before('start runtime', (done) => { runtimeProcess = spawn(prog, [ '--host=localhost', '--port=3471', '--open=false', `--base-dir=${baseDir}`, `--secret=${runtimeSecret}`, '--auto-save=true', ]); runtimeProcess.stdout.pipe(process.stdout); runtimeProcess.stderr.pipe(process.stderr); healthCheck('ws://localhost:3471', done); }); after('stop runtime', (done) => { if (!runtimeProcess) { done(); return; } process.kill(runtimeProcess.pid); done(); }); it('should be possible to connect', () => fbpClient({ address: 'ws://localhost:3471', protocol: 'websocket', secret: runtimeSecret, }) .then((c) => { runtimeClient = c; return c.connect(); })); describe('setting component sources', () => { const source = `const noflo = require('noflo'); exports.getComponent = () => { const c = new noflo.Component(); c.inPorts.add('in'); c.outPorts.add('out'); c.process((input, output) => { output.sendDone(input.getData() + 2); }); return c; };`; const spec = `topic: auto-save/Plusser cases: - name: 'sending a boolean' assertion: 'should repeat the same' inputs: in: true expect: out: equals: true`; const componentPath = path.resolve(__dirname, './fixtures/auto-save/components/Plusser.js'); const specPath = path.resolve(__dirname, './fixtures/auto-save/spec/Plusser.yaml'); let plusserFound = false; after('clean up file', () => { if (!plusserFound) { return Promise.resolve(); } return unlink(componentPath) .then(() => unlink(specPath)); }); it('should be possible to send the source code to the runtime', () => runtimeClient .protocol.component.source({ name: 'Plusser', library: 'auto-save', language: 'javascript', tests: spec, code: source, }) .then(() => new Promise((resolve) => { setTimeout(() => { resolve(); }, 200); }))); it('should have saved the source code to the fixture folder', () => readFile( componentPath, 'utf-8', ) .then((contents) => { plusserFound = true; expect(contents).to.eql(source); })); it('should have saved the fbp-spec file to the fixture folder', () => readFile( specPath, 'utf-8', ) .then((contents) => { expect(contents).to.eql(spec); })); }); describe('setting component sources outside of project', () => { let source; const componentPath = path.resolve(__dirname, './fixtures/auto-save/components/Output.js'); before('read source code', () => readFile( path.resolve(__dirname, '../node_modules/noflo-core/components/Output.js'), 'utf-8', ) .then((contents) => { source = contents; })); it('should be possible to send the source code to the runtime', () => runtimeClient .protocol.component.source({ name: 'Output', library: 'core', language: 'javascript', code: source, })); it('should not have saved the source code to the fixture folder', () => readFile( componentPath, 'utf-8', ) .then( () => Promise.reject(new Error('core/Output was saved unexpectedly')), () => Promise.resolve('No Output.js found, as expected'), )); }); describe('editing a graph without namespaced name', () => { const graphName = 'Test'; const graphPath = path.resolve(__dirname, `./fixtures/auto-save/graphs/${graphName}.json`); const graphInstance = new fbpGraph.Graph(graphName); let graphFound = false; before('set up graph', () => { graphInstance.setProperties({ ...graphInstance.properties, library: 'auto-save', id: graphName, main: false, environment: { type: 'noflo-nodejs', }, }); graphInstance.addNode('one', 'auto-save/Plusser'); graphInstance.addNode('two', 'core/Output'); graphInstance.addEdge('one', 'out', 'two', 'in'); graphInstance.addInitial(1, 'one', 'in'); }); after('clean up file', () => { if (!graphFound) { return Promise.resolve(); } return unlink(graphPath); }); it('should be possible to send a graph to the runtime', () => runtimeClient .protocol.graph.send(graphInstance, false)); it('should have saved the graph JSON to the fixture folder', () => waitFor(200) .then(() => readFile( graphPath, 'utf-8', )) .then((contents) => { graphFound = true; const originalGraphJson = JSON.parse(JSON.stringify(graphInstance.toJSON())); delete originalGraphJson.properties.id; const graphJson = JSON.parse(contents); expect(graphJson).to.eql(originalGraphJson); })); }); describe('editing a graph with namespaced name', () => { const graphName = 'main'; const graphPath = path.resolve(__dirname, './fixtures/auto-save/graphs/main.json'); const graphInstance = new fbpGraph.Graph(graphName); let graphFound = false; before('set up graph', () => { graphInstance.setProperties({ ...graphInstance.properties, library: 'auto-save', id: `default/${graphName}`, main: true, environment: { type: 'noflo-nodejs', }, }); graphInstance.addNode('one', 'auto-save/Plusser'); graphInstance.addNode('two', 'core/Output'); graphInstance.addEdge('one', 'out', 'two', 'in'); graphInstance.addInitial(1, 'one', 'in'); }); after('clean up file', () => { if (!graphFound) { return Promise.resolve(); } return unlink(graphPath); }); it('should be possible to send a graph to the runtime', () => runtimeClient .protocol.graph.send({ ...graphInstance, name: 'default/main', }, true)); it('should have saved the graph JSON to the fixture folder', () => waitFor(200) .then(() => readFile( graphPath, 'utf-8', )) .then((contents) => { graphFound = true; const originalGraphJson = JSON.parse(JSON.stringify(graphInstance.toJSON())); delete originalGraphJson.properties.id; const graphJson = JSON.parse(contents); expect(graphJson).to.eql(originalGraphJson); })); }); }); describe('--protocol=webrtc', () => { const baseDir = path.resolve(__dirname, './fixtures/graph-as-component'); const graph = path.resolve(baseDir, './graphs/helloin.fbp'); let runtimeProcess; let runtimeClient; const runtimeId = uuid(); before('start runtime', (done) => { runtimeProcess = spawn(prog, [ '--open=false', `--id=${runtimeId}`, '--protocol=webrtc', `--secret=${runtimeSecret}`, `--base-dir=${baseDir}`, `--graph=${graph}`, ]); runtimeProcess.stdout.pipe(process.stdout); runtimeProcess.stderr.pipe(process.stderr); done(); }); it('should be possible to connect', function () { this.timeout(6000); return fbpClient({ address: runtimeId, protocol: 'webrtc', secret: runtimeSecret, }, { connectionTimeout: 5000, }) .then((c) => { runtimeClient = c; return c.connect(); }); }); it('should have marked the graph as the main', () => { expect(runtimeClient.definition.graph).to.equal('graph-as-component/HelloIn'); }); it('should be possible to get graph sources', () => runtimeClient .protocol.component.getsource({ name: 'graph-as-component/HelloIn', })); after('stop runtime', (done) => { if (!runtimeProcess) { done(); return; } process.kill(runtimeProcess.pid); done(); }); }); });