planify
Version:
Plan a series of steps and display the output in a beautiful way
639 lines (538 loc) • 23.2 kB
JavaScript
;
const expect = require('chai').expect;
const Promise = require('bluebird');
const planify = require('../');
const bufferStdio = require('./helpers/buffer-stdio');
describe('functional', () => {
describe('.getNode()', () => {
it('should return the plan node (root node)', () => {
const node = planify().getNode();
expect(node).to.be.an('object');
expect(node.type).to.equal('plan');
});
});
describe('.phase()', () => {
it('should be able to create phases', () => {
const plan = planify();
plan.phase('phase label', (phase) => {
expect(phase).to.have.all.keys('phase', 'step', 'merge');
});
const node = plan.getNode();
expect(node.children).to.have.length(1);
expect(node.children[0].type).to.equal('phase');
expect(node.children[0].label).to.equal('phase label');
});
it('should allow chaining', () => {
const plan = planify();
const ret = plan.phase('phase label', (phase) => {
const ret = phase.phase('phase of phase label', () => {});
expect(ret).to.equal(phase);
});
expect(ret).to.equal(plan);
});
});
describe('.step()', () => {
it('should be able to create steps', () => {
const plan = planify();
plan.step('step label', () => {});
plan.phase('phase label', (phase) => {
phase.step('phase step label', () => {});
});
const node = plan.getNode();
expect(node.children).to.have.length(2);
expect(node.children[0].type).to.equal('step');
expect(node.children[0].label).to.equal('step label');
expect(node.children[1].type).to.equal('phase');
expect(node.children[1].label).to.equal('phase label');
expect(node.children[1].children).to.have.length(1);
expect(node.children[1].children[0].type).to.equal('step');
expect(node.children[1].children[0].label).to.equal('phase step label');
expect(node.steps).to.have.length(2);
expect(node.steps[0]).to.equal(node.children[0]);
expect(node.steps[1]).to.equal(node.children[1].children[0]);
});
it('should set the step options', () => {
const plan = planify();
plan.step('step label', { mute: true, slow: 10000 }, () => {});
const step = plan.getNode().children[0];
expect(step.type).to.equal('step');
expect(step.options.mute).to.eql({ stdout: true, stderr: true });
expect(step.options.slow).to.equal(10000);
});
it('should allow chaining', () => {
const plan = planify();
const ret = plan.step('step label', () => {});
expect(ret).to.equal(plan);
});
});
describe('.run()', () => {
it('should successfully run a simple plan', () => {
return planify()
.step('step 1', (data) => {
data.step1 = 'foo';
})
.step('step 2', (data, done) => {
data.step2 = 'foo';
setTimeout(done, 50);
})
.step('step 3', (data) => {
return Promise.delay(50)
.then(() => { data.step3 = 'foo'; });
})
.run({ reporter: 'silent' })
.then((data) => {
expect(data).to.eql({
step1: 'foo',
step2: 'foo',
step3: 'foo',
});
});
});
it('should fail if one of the steps failed', () => {
return planify()
.step('step 1', () => { throw new Error('foo'); })
.run({ reporter: 'silent' })
.then(() => {
throw new Error('Should have failed');
}, (err) => {
expect(err).to.be.an.instanceOf(Error);
expect(err.message).to.equal('foo');
})
.then(() => {
return planify()
.step('step 1', (data, done) => done(new Error('foo')))
.run({ reporter: 'silent' });
})
.then(() => {
throw new Error('Should have failed');
}, (err) => {
expect(err).to.be.an.instanceOf(Error);
expect(err.message).to.equal('foo');
})
.then(() => {
return planify()
.step('step 1', () => Promise.reject(new Error('foo')))
.run({ reporter: 'silent' });
})
.then(() => {
throw new Error('Should have failed');
}, (err) => {
expect(err).to.be.an.instanceOf(Error);
expect(err.message).to.equal('foo');
});
});
it('should accept callbacks', (next) => {
planify()
.step('step 1', () => {})
.run({ reporter: 'silent' }, (err) => {
expect(err).to.not.be.ok;
next();
});
});
it('should fail if trying to run plan while already running', () => {
const plan = planify()
.step('step 1', () => Promise.delay(100));
const promise = plan.run({ reporter: 'silent' });
expect(() => plan.run({ reporter: 'silent' })).to.throw('A plan is already running');
return promise;
});
it('should fail if trying to run two plans simultaneously', () => {
const promise = planify()
.step('step 1', () => {
return Promise.delay(100);
})
.run({ reporter: 'silent' });
expect(() => {
planify()
.step('step 1', () => {})
.run({ reporter: 'silent' });
}).to.throw('A plan is already running');
return promise;
});
it('should not allow any more phases or steps to be added if running', () => {
let asserts = 0;
let deepPhase;
const plan = planify()
.step('step 1', () => {
return Promise.delay(50);
})
.phase('phase 1', (phase) => {
deepPhase = phase;
});
const promise = plan.run({ reporter: 'silent' });
setImmediate(() => {
expect(() => {
asserts += 1;
plan.step('step 2', () => {});
}).to.throw('Can\'t modify plan when is already running');
});
setImmediate(() => {
expect(() => {
asserts += 1;
plan.phase('phase 1', () => {});
}).to.throw('Can\'t modify plan when is already running');
});
setImmediate(() => {
expect(() => {
asserts += 1;
deepPhase.step('phase 1 step 1', () => {});
}).to.throw('Can\'t modify plan when is already running');
});
setImmediate(() => {
expect(() => {
asserts += 1;
deepPhase.phase('phase 1 phase 1', () => {});
}).to.throw('Can\'t modify plan when is already running');
});
return promise
.then(() => {
expect(asserts).to.equal(4);
});
});
it('should run the plan with the specified initial data and resolve with it', () => {
const initialData = { foo: 'bar' };
let stepData;
return planify(initialData)
.step('step 1', (data) => { stepData = data; })
.run({ reporter: 'silent' })
.then((finalData) => {
expect(finalData).to.equal(initialData);
expect(stepData).to.equal(initialData);
});
});
});
describe('.merge()', () => {
it('should merge a plan into a plan', () => {
const plan1 = planify({ foo: 'foo' })
.step('plan1.step1', () => {})
.phase('plan1.phase1', (phase) => {
phase
.step('plan1.phase1.step1', () => {})
.step('plan1.phase1.step2', () => {});
});
const plan2 = planify({ bar: 'bar' })
.step('plan2.step1', () => {})
.phase('plan2.phase1', (phase) => {
phase
.step('plan2.phase1.step1', () => {})
.step('plan2.phase1.step2', () => {});
});
plan1.merge(plan2);
const plan1Node = plan1.getNode();
const plan2Node = plan2.getNode();
expect(plan1Node.data).to.eql({ foo: 'foo', bar: 'bar' });
expect(plan1Node.steps).to.have.length(6);
expect(plan1Node.children).to.have.length(4);
expect(plan2Node.children).to.have.length(0);
plan1Node.children.forEach((child) => expect(child.parent).to.equal(plan1Node));
expect(plan1Node.children[0].label).to.equal('plan1.step1');
expect(plan1Node.children[1].label).to.equal('plan1.phase1');
expect(plan1Node.children[2].label).to.equal('plan2.step1');
expect(plan1Node.children[3].label).to.equal('plan2.phase1');
expect(plan1Node.children[0].plan).to.equal(plan1Node);
expect(plan1Node.children[1].children[0].plan).to.equal(plan1Node);
expect(plan1Node.children[1].children[1].plan).to.equal(plan1Node);
expect(plan1Node.children[2].plan).to.equal(plan1Node);
expect(plan1Node.children[3].children[0].plan).to.equal(plan1Node);
expect(plan1Node.children[3].children[1].plan).to.equal(plan1Node);
});
it('should merge a plan into a phase', () => {
const plan1 = planify({ foo: 'foo' })
.step('plan1.step1', () => {})
.phase('plan1.phase1', (phase) => {
phase
.step('plan1.phase1.step1', () => {})
.step('plan1.phase1.step2', () => {});
});
const plan2 = planify({ bar: 'bar' })
.step('plan2.step1', () => {})
.phase('plan2.phase1', (phase) => {
phase
.step('plan2.phase1.step1', () => {})
.step('plan2.phase1.step2', () => {})
.merge(plan1);
});
const plan1Node = plan1.getNode();
const plan2Node = plan2.getNode();
expect(plan2Node.data).to.eql({ foo: 'foo', bar: 'bar' });
expect(plan2Node.steps).to.have.length(6);
expect(plan2Node.children).to.have.length(2);
expect(plan1Node.children).to.have.length(0);
plan2Node.children.forEach((child) => expect(child.parent).to.equal(plan2Node));
expect(plan2Node.children[0].label).to.equal('plan2.step1');
expect(plan2Node.children[1].label).to.equal('plan2.phase1');
expect(plan2Node.children[1].children[0].label).to.equal('plan2.phase1.step1');
expect(plan2Node.children[1].children[1].label).to.equal('plan2.phase1.step2');
expect(plan2Node.children[1].children[2].label).to.equal('plan1.step1');
expect(plan2Node.children[1].children[3].label).to.equal('plan1.phase1');
expect(plan2Node.children[1].children[3].children).to.have.length(2);
});
it('should fail if src is not a plan', () => {
const plan = planify({ foo: 'foo' });
let phase;
planify({ bar: 'bar' })
.phase('some phase', (phase_) => {
phase = phase_;
});
expect(() => {
plan.merge(phase);
}).to.throw('Can only merge a plan');
});
});
describe('options', () => {
it('should use the options.reporter as an object', () => {
let ok = false;
const reporter = {
plan: {
start: () => { ok = true; },
},
};
return planify()
.step('step 1', () => {})
.run({ reporter })
.then(() => {
expect(ok).to.equal(true);
});
});
it.skip('should use the options.reporter as a string');
it('should throw an appropriate error if options.reporter does not exist', () => {
expect(() => {
planify()
.run({ reporter: 'somethingthatwillneverexist' });
}).to.throw('Unknown reporter: somethingthatwillneverexist');
});
it('should throw an appropriate error if options.reporter is not a plain object', () => {
function Foo() {}
expect(() => {
planify()
.run({ reporter: new Foo() });
}).to.throw('Reporter must be a string or a plain object');
});
it('should exit automatically with an appropriate exit code if options.exit is set to true', () => {
const exitCodes = [];
const originalExit = process.exit;
process.exit = (code) => { exitCodes.push(code); };
return planify()
.step('step 1', () => {})
.run({ reporter: 'silent', exit: true })
.then(() => {
return planify()
.step('step 1', () => { throw new Error('foo'); })
.run({ reporter: 'silent', exit: true });
})
.catch(() => {
return planify()
.step('step 1', () => {
const err = new Error('foo');
err.exitCode = 25;
throw err;
})
.run({ reporter: 'silent', exit: true });
})
.then(() => {
throw new Error('Should have failed');
}, (err) => {
expect(err.message).to.eql('foo');
expect(exitCodes).to.eql([0, 1, 25]);
})
.finally(() => {
process.exit = originalExit;
});
});
});
describe('step options', () => {
it('should keep running if options.fatal is false', () => {
let stepError;
const reporter = {
step: {
fail(step, err) { stepError = err; },
},
};
return planify()
.step('step 1', { fatal: false }, () => { throw new Error('foo'); })
.step('step 2', (data) => { data.step2 = 'foo'; })
.run({ reporter })
.then((data) => {
expect(stepError).to.be.an.instanceOf(Error);
expect(stepError.message).to.equal('foo');
expect(data).to.eql({ step2: 'foo' });
});
});
it('should calculate the speed correctly based on options.slow', () => {
const speeds = [];
const reporter = {
step: {
finish(step) { speeds.push(step.info.speed); },
},
};
return planify()
.step('step 1', { slow: 100 }, () => { return Promise.delay(101); })
.step('step 3', { slow: 500 }, () => { return Promise.delay(251); })
.step('step 4', { slow: 500 }, () => { return Promise.delay(10); })
.run({ reporter })
.then(() => {
expect(speeds).to.eql(['slow', 'medium', 'fast']);
});
});
describe('options.mute', () => {
function testMuteOption(options) {
const called = { stdout: false, stderr: false };
const reporter = {
step: {
write: {
stdout() { called.stdout = true; },
stderr() { called.stderr = true; },
},
},
};
bufferStdio.start();
return planify()
.step('step 1', options, () => {
console.log('write to stdout');
console.error('write to stderr');
process.stdout.write('write to stdout\n');
process.stderr.write('write to stderr\n');
})
.run({ reporter })
.finally(() => {
const buffered = bufferStdio.finish();
expect(buffered.stdout).to.equal('');
expect(buffered.stderr).to.equal('');
})
.return(called);
}
it('should mute stdout & stderr if both are set to true', () => {
return testMuteOption({ mute: true })
.then((called) => {
expect(called).to.eql({ stdout: false, stderr: false });
return testMuteOption({ mute: { stdout: true, stderr: true } });
})
.then((called) => {
expect(called).to.eql({ stdout: false, stderr: false });
});
});
it('should NOT mute stdout & stderr if both are set to falsy', () => {
return testMuteOption({ mute: false })
.then((called) => {
expect(called).to.eql({ stdout: true, stderr: true });
return testMuteOption({ mute: null });
})
.then((called) => {
expect(called).to.eql({ stdout: true, stderr: true });
return testMuteOption({ mute: { stdout: null, stderr: undefined } });
})
.then((called) => {
expect(called).to.eql({ stdout: true, stderr: true });
return testMuteOption({ mute: { stdout: null, stderr: undefined } });
})
.then((called) => {
expect(called).to.eql({ stdout: true, stderr: true });
return testMuteOption();
})
.then((called) => {
expect(called).to.eql({ stdout: true, stderr: true });
});
});
it('should only mute stdout if only options.mute.stdout is true', () => {
return testMuteOption({ mute: { stdout: true, stderr: false } })
.then((called) => {
expect(called).to.eql({ stdout: false, stderr: true });
return testMuteOption({ mute: { stdout: true, stderr: null } });
})
.then((called) => {
expect(called).to.eql({ stdout: false, stderr: true });
});
});
it('should only mute stderr if only options.mute.stderr is true', () => {
return testMuteOption({ mute: { stdout: false, stderr: true } })
.then((called) => {
expect(called).to.eql({ stdout: true, stderr: false });
return testMuteOption({ mute: { stdout: null, stderr: true } });
})
.then((called) => {
expect(called).to.eql({ stdout: true, stderr: false });
});
});
});
});
describe('stdio hook', () => {
it('should hook stdout & stderr and forward it to the reporter', () => {
const output = { stdout: '', stderr: '' };
const reporter = {
step: {
write: {
stdout(step, str) { output.stdout += str; },
stderr(step, str) { output.stderr += str; },
},
},
};
return planify()
.step('step 1', () => {
console.log('write to stdout');
process.stdout.write('another write to stdout\n');
console.error('write to stderr');
process.stderr.write('another write to stderr\n');
})
.run({ reporter })
.then(() => {
expect(output).to.eql({
stdout: 'write to stdout\nanother write to stdout\n',
stderr: 'write to stderr\nanother write to stderr\n',
});
});
});
it('should unhook stdio on uncaught exceptions', (next) => {
const listeners = process.listeners('uncaughtException');
const mochaListener = listeners[listeners.length - 1];
let uncaughtException;
// Remove mocha listener & track uncaught exception
process.removeListener('uncaughtException', mochaListener);
process.once('uncaughtException', (err) => { uncaughtException = err; });
planify()
.step('step 1', () => {
setTimeout(() => {
process.nextTick(() => {
// Restore listeners exactly how they were, including order
process.removeAllListeners('uncaughtException');
listeners.forEach((listener) => process.on('uncaughtException', listener));
expect(uncaughtException).to.be.an.instanceOf(Error);
expect(uncaughtException.message).to.equal('foo');
setTimeout(next, 50);
});
throw new Error('foo');
}, 25);
return Promise.delay(50);
})
.run({ reporter: 'silent' });
});
it('should work well with buffers', () => {
const output = { stdout: '', stderr: '' };
const reporter = {
step: {
write: {
stdout(step, buffer) {
expect(buffer).to.be.an.instanceOf(Buffer);
output.stdout += buffer;
},
stderr(step, buffer) {
expect(buffer).to.be.an.instanceOf(Buffer);
output.stderr += buffer;
},
},
},
};
return planify()
.step('step 1', () => {
process.stdout.write(new Buffer('write to stdout\n'));
process.stderr.write(new Buffer('write to stderr\n'));
})
.run({ reporter })
.then(() => {
expect(output).to.eql({
stdout: 'write to stdout\n',
stderr: 'write to stderr\n',
});
});
});
});
});