noflo
Version:
Flow-Based Programming environment for JavaScript
487 lines (479 loc) • 15.3 kB
JavaScript
describe('asPromise interface', () => {
let loader = null;
const processAsync = function () {
const c = new noflo.Component();
c.inPorts.add('in',
{ datatype: 'string' });
c.outPorts.add('out',
{ datatype: 'string' });
return c.process((input, output) => {
const data = input.getData('in');
setTimeout(() => output.sendDone(data),
1);
});
};
const processError = function () {
const c = new noflo.Component();
c.inPorts.add('in',
{ datatype: 'string' });
c.outPorts.add('out',
{ datatype: 'string' });
c.outPorts.add('error');
return c.process((input, output) => {
const data = input.getData('in');
output.done(new Error(`Received ${data}`));
});
};
const processValues = function () {
const c = new noflo.Component();
c.inPorts.add('in', {
datatype: 'string',
values: ['green', 'blue'],
});
c.outPorts.add('out',
{ datatype: 'string' });
return c.process((input, output) => {
const data = input.getData('in');
output.sendDone(data);
});
};
const neverSend = function () {
const c = new noflo.Component();
c.inPorts.add('in',
{ datatype: 'string' });
c.outPorts.add('out',
{ datatype: 'string' });
return c.process((input) => {
input.getData('in');
});
};
const streamify = function () {
const c = new noflo.Component();
c.inPorts.add('in',
{ datatype: 'string' });
c.outPorts.add('out',
{ datatype: 'string' });
c.process((input, output) => {
const data = input.getData('in');
const words = data.split(' ');
for (let idx = 0; idx < words.length; idx++) {
const word = words[idx];
output.send(new noflo.IP('openBracket', idx));
const chars = word.split('');
for (const char of chars) { output.send(new noflo.IP('data', char)); }
output.send(new noflo.IP('closeBracket', idx));
}
output.done();
});
return c;
};
before(() => {
loader = new noflo.ComponentLoader(baseDir);
return loader
.listComponents()
.then(() => {
loader.registerComponent('process', 'Async', processAsync);
loader.registerComponent('process', 'Error', processError);
loader.registerComponent('process', 'Values', processValues);
loader.registerComponent('process', 'NeverSend', neverSend);
loader.registerComponent('process', 'Streamify', streamify);
});
});
describe('with a non-existing component', () => {
let wrapped = null;
before(() => {
wrapped = noflo.asPromise('foo/Bar',
{ loader });
});
it('should be able to wrap it', (done) => {
chai.expect(wrapped).to.be.a('function');
chai.expect(wrapped.length).to.equal(1);
done();
});
it('should fail execution', () => wrapped(1)
.then(() => {
throw new Error('Unexpected pass');
}, (err) => {
chai.expect(err).to.be.an('error');
}));
});
describe('with simple asynchronous component', () => {
let wrapped = null;
before(() => {
wrapped = noflo.asPromise('process/Async',
{ loader });
});
it('should be able to wrap it', (done) => {
chai.expect(wrapped).to.be.a('function');
chai.expect(wrapped.length).to.equal(1);
done();
});
it('should execute network with input map and provide output map', () => {
const expected = { hello: 'world' };
return wrapped({
in: expected,
})
.then((out) => {
chai.expect(out.out).to.eql(expected);
});
});
it('should execute network with simple input and provide simple output', () => {
const expected = { hello: 'world' };
return wrapped(expected)
.then((out) => {
chai.expect(out).to.eql(expected);
});
});
it('should not mix up simultaneous runs', (done) => {
let received = 0;
for (let idx = 0; idx <= 100; idx += 1) {
/* eslint-disable no-loop-func */
wrapped(idx)
.then((out) => {
chai.expect(out).to.equal(idx);
received++;
if (received !== 101) { return; }
done();
}, done);
}
});
it('should execute a network with a sequence and provide output sequence', () => {
const sent = [
{ in: 'hello' },
{ in: 'world' },
{ in: 'foo' },
{ in: 'bar' },
];
const expected = sent.map((portmap) => ({ out: portmap.in }));
return wrapped(sent)
.then((out) => {
chai.expect(out).to.eql(expected);
});
});
describe('with the raw option', () => {
it('should execute a network with a sequence and provide output sequence', () => {
const wrappedRaw = noflo.asPromise('process/Async', {
loader,
raw: true,
});
const sent = [
{ in: new noflo.IP('openBracket', 'a') },
{ in: 'hello' },
{ in: 'world' },
{ in: new noflo.IP('closeBracket', 'a') },
{ in: new noflo.IP('openBracket', 'b') },
{ in: 'foo' },
{ in: 'bar' },
{ in: new noflo.IP('closeBracket', 'b') },
];
return wrappedRaw(sent)
.then((out) => {
const types = out.map((map) => `${map.out.type} ${map.out.data}`);
chai.expect(types).to.eql([
'openBracket a',
'data hello',
'data world',
'closeBracket a',
'openBracket b',
'data foo',
'data bar',
'closeBracket b',
]);
});
});
});
});
describe('with a component sending an error', () => {
let wrapped = null;
before(() => {
wrapped = noflo.asPromise('process/Error',
{ loader });
});
it('should execute network with input map and provide error', () => {
const expected = 'hello there';
return wrapped({
in: expected,
})
.then(() => {
throw new Error('Received unexpected output');
}, (err) => {
chai.expect(err).to.be.an('error');
chai.expect(err.message).to.contain(expected);
});
});
it('should execute network with simple input and provide error', () => {
const expected = 'hello world';
return wrapped(expected)
.then(() => {
throw new Error('Received unexpected output');
}, (err) => {
chai.expect(err).to.be.an('error');
chai.expect(err.message).to.contain(expected);
});
});
});
describe('with a component supporting only certain values', () => {
let wrapped = null;
before(() => {
wrapped = noflo.asPromise('process/Values',
{ loader });
});
it('should execute network with input map and provide output map', () => {
const expected = 'blue';
return wrapped({
in: expected,
})
.then((out) => {
chai.expect(out.out).to.eql(expected);
});
});
it('should execute network with simple input and provide simple output', () => {
const expected = 'blue';
return wrapped(expected)
.then((out) => {
chai.expect(out).to.eql(expected);
});
});
it('should execute network with wrong map and provide error', () => wrapped({
in: 'red',
})
.then(() => {
throw new Error('Received unexpected output');
}, (err) => {
chai.expect(err).to.be.an('error');
chai.expect(err.message).to.contain('Invalid data=\'red\' received, not in [green,blue]');
}));
it('should execute network with wrong input and provide error', () => wrapped('red')
.then(() => {
throw new Error('Received unexpected output');
}, (err) => {
chai.expect(err).to.be.an('error');
chai.expect(err.message).to.contain('Invalid data=\'red\' received, not in [green,blue]');
}));
});
describe('with a component sending streams', () => {
let wrapped = null;
before(() => {
wrapped = noflo.asPromise('process/Streamify',
{ loader });
});
it('should execute network with input map and provide output map with streams as arrays', () => wrapped({
in: 'hello world',
})
.then((out) => {
chai.expect(out.out).to.eql([
['h', 'e', 'l', 'l', 'o'],
['w', 'o', 'r', 'l', 'd'],
]);
}));
it('should execute network with simple input and and provide simple output with streams as arrays', () => wrapped('hello there')
.then((out) => {
chai.expect(out).to.eql([
['h', 'e', 'l', 'l', 'o'],
['t', 'h', 'e', 'r', 'e'],
]);
}));
describe('with the raw option', () => {
it('should execute network with input map and provide output map with IP objects', () => {
const wrappedRaw = noflo.asPromise('process/Streamify', {
loader,
raw: true,
});
return wrappedRaw({
in: 'hello world',
})
.then((out) => {
const types = out.out.map((ip) => `${ip.type} ${ip.data}`);
chai.expect(types).to.eql([
'openBracket 0',
'data h',
'data e',
'data l',
'data l',
'data o',
'closeBracket 0',
'openBracket 1',
'data w',
'data o',
'data r',
'data l',
'data d',
'closeBracket 1',
]);
});
});
});
});
describe('with a graph instead of component name', () => {
let graph = null;
let wrapped = null;
before((done) => {
noflo.graph.loadFBP(`\
INPORT=Async.IN:IN
OUTPORT=Stream.OUT:OUT
Async(process/Async) OUT -> IN Stream(process/Streamify)\
`, (err, g) => {
if (err) {
done(err);
return;
}
graph = g;
wrapped = noflo.asPromise(graph,
{ loader });
done();
});
});
it('should execute network with input map and provide output map with streams as arrays', () => wrapped({
in: 'hello world',
})
.then((out) => {
chai.expect(out.out).to.eql([
['h', 'e', 'l', 'l', 'o'],
['w', 'o', 'r', 'l', 'd'],
]);
}));
it('should execute network with simple input and and provide simple output with streams as arrays', () => wrapped('hello there')
.then((out) => {
chai.expect(out).to.eql([
['h', 'e', 'l', 'l', 'o'],
['t', 'h', 'e', 'r', 'e'],
]);
}));
});
describe('with a graph containing a component supporting only certain values', () => {
let graph = null;
let wrapped = null;
before((done) => {
noflo.graph.loadFBP(`\
INPORT=Async.IN:IN
OUTPORT=Values.OUT:OUT
Async(process/Async) OUT -> IN Values(process/Values)\
`, (err, g) => {
if (err) {
done(err);
return;
}
graph = g;
wrapped = noflo.asPromise(graph,
{ loader });
done();
});
});
it('should execute network with input map and provide output map', () => {
const expected = 'blue';
return wrapped({
in: expected,
})
.then((out) => {
chai.expect(out.out).to.eql(expected);
});
});
it('should execute network with simple input and provide simple output', () => {
const expected = 'blue';
return wrapped(expected)
.then((out) => {
chai.expect(out).to.eql(expected);
});
});
it('should execute network with wrong map and provide error', () => wrapped({
in: 'red',
})
.then(() => {
throw new Error('Unexpected pass');
}, (err) => {
chai.expect(err).to.be.an('error');
chai.expect(err.message).to.contain('Invalid data=\'red\' received, not in [green,blue]');
}));
it('should execute network with wrong input and provide error', () => wrapped('red')
.then(() => {
throw new Error('Unexpected pass');
}, (err) => {
chai.expect(err).to.be.an('error');
chai.expect(err.message).to.contain('Invalid data=\'red\' received, not in [green,blue]');
}));
});
describe('with networkCallback option', () => {
let wrapped = null;
let called = 0;
let started = 0;
afterEach(() => {
called = 0;
started = 0;
});
it('should not provide network at callbackization time', (done) => {
chai.expect(called).to.equal(0);
wrapped = noflo.asPromise('process/Async', {
loader,
networkCallback: (network) => {
network.on('start', () => {
started++;
});
called++;
},
});
chai.expect(wrapped).to.be.a('function');
chai.expect(called).to.equal(0);
done();
});
it('should provide the network to the callback when executed', () => {
const expected = { hello: 'world' };
chai.expect(called).to.equal(0);
return wrapped(expected)
.then((out) => {
chai.expect(out).to.eql(expected);
chai.expect(called).to.equal(1);
});
});
it('should provide the network before actual execution so that we catch the start event', () => {
const expected = { hello: 'world' };
chai.expect(called).to.equal(0);
chai.expect(started).to.equal(0);
return wrapped(expected)
.then((out) => {
chai.expect(out).to.eql(expected);
chai.expect(called).to.equal(1);
chai.expect(started).to.equal(1);
});
});
});
describe('with flowtrace option', () => {
it('should store a trace for a simple component execution', () => {
const trace = new flowtrace.Flowtrace();
const wrapped = noflo.asPromise('process/Async', {
loader,
flowtrace: trace,
});
return wrapped('hello')
.then((out) => {
chai.expect(out).to.equal('hello');
const collectedTrace = trace.toJSON();
chai.expect(collectedTrace.header.metadata).to.include.keys(['start', 'end']);
chai.expect(collectedTrace.header.graphs['process/Async']).to.be.an('object');
chai.expect(collectedTrace.header.main).to.equal('process/Async');
const eventTypes = collectedTrace.events.map((e) => `${e.protocol}:${e.command}`);
chai.expect(eventTypes).to.eql([
'network:started',
'network:data',
'network:data',
'network:stopped',
]);
chai.expect(JSON.parse(JSON.stringify(collectedTrace.events[1].payload))).to.eql({
data: 'hello',
src: null,
tgt: {
node: 'process/Async',
port: 'in',
},
});
chai.expect(JSON.parse(JSON.stringify(collectedTrace.events[2].payload))).to.eql({
data: 'hello',
src: {
node: 'process/Async',
port: 'out',
},
tgt: null,
});
});
});
});
});