automaton
Version:
Task automation tool
1,488 lines (1,365 loc) • 49.7 kB
JavaScript
/*global describe, it, after*/
;
var expect = require('expect.js');
var fs = require('fs');
var async = require('async');
var isDir = require('./helpers/util/isDir');
var callbackTask = require('./helpers/tasks/callback');
var removeColors = require('../lib/Logger').removeColors;
module.exports = function (automaton) {
describe('Engine', function () {
after(function () {
automaton.removeTask('foo');
});
it('should throw if a task is invalid', function () {
// not an object
expect(function () {
automaton.addTask('foo');
}).to.throwException(/an object/);
// no subtasks
expect(function () {
automaton.addTask({});
}).to.throwException(/subtasks to be an array/);
// no id
expect(function () {
automaton.addTask({
tasks: []
});
}).to.throwException(/only add tasks with an id/);
// empty id
expect(function () {
automaton.addTask({
id: '',
tasks: []
});
}).to.throwException(/empty/);
// invalid name/author
expect(function () {
automaton.addTask({
id: 'foo',
name: 1,
tasks: []
});
}).to.throwException(/name/);
expect(function () {
automaton.addTask({
id: 'foo',
author: 1,
tasks: []
});
}).to.throwException(/author/);
// empty name/author
expect(function () {
automaton.addTask({
id: 'foo',
name: '',
tasks: []
});
}).to.throwException(/empty/);
expect(function () {
automaton.addTask({
id: 'foo',
author: '',
tasks: []
});
}).to.throwException(/empty/);
expect(function () { // test valid case
automaton.addTask({
id: 'foo',
author: 'André',
tasks: []
});
}).to.not.throwException();
// invalid description
expect(function () {
automaton.addTask({
id: 'foo',
description: 1,
tasks: []
});
}).to.throwException(/description/);
expect(function () {
automaton.addTask({
id: 'foo',
description: function () {},
tasks: []
});
}).to.throwException(/description/);
expect(function () { // test valid case
automaton.addTask({
id: 'foo',
description: 'Some task',
tasks: []
});
}).to.not.throwException();
expect(function () {
automaton.addTask({
id: 'foo',
description: null,
tasks: []
});
}).to.not.throwException();
// invalid options
expect(function () {
automaton.addTask({
id: 'foo',
options: 1,
tasks: []
});
}).to.throwException(/options/);
expect(function () { // test valid case
automaton.addTask({
id: 'foo',
options: {},
tasks: []
});
}).to.not.throwException();
expect(function () {
automaton.addTask({
id: 'foo',
options: {
opt1: 1
},
tasks: []
});
}).to.throwException(/options/);
expect(function () {
automaton.addTask({
id: 'foo',
options: {
opt1: {
description: 1
}
},
tasks: []
});
}).to.throwException(/option description/);
expect(function () {
automaton.addTask({
id: 'foo',
options: {
opt1: {
description: function () {}
}
},
tasks: []
});
}).to.throwException(/option description/);
expect(function () { // test valid case
automaton.addTask({
id: 'foo',
options: {
opt1: {
description: 'test'
}
},
tasks: []
});
}).to.not.throwException();
// test setup
expect(function () {
automaton.addTask({
id: 'foo',
setup: 1,
tasks: []
});
}).to.throwException(/setup/);
expect(function () { // test valid case
automaton.addTask({
id: 'foo',
setup: function () {},
tasks: []
});
}).to.not.throwException();
// test teardown
expect(function () {
automaton.addTask({
id: 'foo',
teardown: 1,
tasks: []
});
}).to.throwException(/teardown/);
expect(function () { // test valid case
automaton.addTask({
id: 'foo',
teardown: function () {},
tasks: []
});
}).to.not.throwException();
// test tasks
expect(function () {
automaton.addTask({
id: 'foo',
tasks: [1]
});
}).to.throwException();
expect(function () {
automaton.addTask({
id: 'foo',
tasks: [{}]
});
}).to.throwException(/task/);
expect(function () {
automaton.addTask({
id: 'foo',
tasks: [
{
task: 1
}
]
});
}).to.throwException(/task/);
expect(function () { // test valid case
automaton.addTask({
id: 'foo',
tasks: [
{
task: 'cp'
}
]
});
}).to.not.throwException();
expect(function () {
automaton.addTask({
id: 'foo',
tasks: [
{
task: 'cp',
options: 1
}
]
});
}).to.throwException(/options/);
expect(function () {
automaton.addTask({
id: 'foo',
tasks: [
{
task: {
tasks: []
},
options: 1
}
]
});
}).to.throwException(/options/);
expect(function () { // test valid case
automaton.addTask({
id: 'foo',
tasks: [
{
task: 'cp',
options: {}
}
]
});
}).to.not.throwException();
expect(function () {
automaton.addTask({
id: 'foo',
tasks: [
{
task: 'cp',
description: 1
}
]
});
}).to.throwException(/description/);
expect(function () { // test valid case
automaton.addTask({
id: 'foo',
tasks: [
{
task: 'cp',
description: 'copy something'
}
]
});
}).to.not.throwException();
expect(function () {
automaton.addTask({
id: 'foo',
tasks: [
{
task: 'cp',
description: function () {}
}
]
});
}).to.not.throwException();
expect(function () {
automaton.addTask({
id: 'foo',
tasks: [
{
task: {
tasks: []
},
description: function () {}
}
]
});
}).to.not.throwException();
expect(function () {
automaton.addTask({
id: 'foo',
tasks: [
{
task: 'cp',
description: null
}
]
});
}).to.not.throwException();
expect(function () {
automaton.addTask({
id: 'foo',
tasks: [
{
task: {
tasks: []
},
grunt: 1
}
]
});
}).to.throwException(/grunt/);
expect(function () { // test valid case
automaton.addTask({
id: 'foo',
tasks: [
{
task: 'cp',
grunt: true
}
]
});
}).to.not.throwException();
expect(function () {
automaton.addTask({
id: 'foo',
tasks: [
{
task: 'cp',
grunt: {}
}
]
});
}).to.not.throwException();
// test deep tasks validation
expect(function () {
automaton.addTask({
id: 'foo',
tasks: [
{
task: {
tasks: 1
},
description: 'copy something'
}
]
});
}).to.throwException(/tasks/);
// test that run also triggers validation for not loaded/added tasks
expect(function () {
automaton.run({
tasks: [
{
task: {
tasks: 1
},
description: 'copy something'
}
]
});
}).to.throwException(/tasks/);
expect(function () {
automaton.run({
tasks: [
{
task: {
tasks: [
{
task: {
tasks: 1
},
description: 'deep'
}
]
},
description: 'copy something'
}
]
});
}).to.throwException(/tasks/);
});
it('should add tasks by id', function (done) {
var called = 0;
automaton.addTask({
id: 'callback',
tasks: []
});
automaton.run({
tasks: [
{
task: 'callback',
options: {
callback: function () {
called++;
}
}
}
]
}, null, function (err) {
automaton.addTask(callbackTask);
if (err) {
throw err;
}
expect(called === 0).to.equal(true);
automaton.run({
tasks: [
{
task: 'callback',
options: {
callback: function () {
called++;
}
}
}
]
}, null, function (err) {
if (err) {
throw err;
}
expect(called === 1).to.equal(true);
done();
});
});
});
it('should remove tasks by id', function () {
automaton.removeTask('callback');
expect(function () {
automaton.run({
tasks: [
{
task: 'callback'
}
]
});
}).to.throwException(/task handler suitable/);
automaton.addTask(callbackTask);
});
it('should load tasks in folder', function () {
// for now this test does not need verifications
// because the root test is loading the tasks folder
// that has the callback task that is being used all over the place
//
// if we ever support loading nested folders, then the test must account for that
});
// test run
it('should error out when attempting to run a not loaded task', function () {
expect(function () {
automaton.run('taskthatwillneverexist');
}).to.throwException(/task handler suitable/);
expect(function () {
automaton.run({
tasks: [
{
task: 'taskthatwillneverexist'
}
]
});
}).to.throwException(/task handler suitable/);
});
it('should be able to run task by id', function (done) {
var dirname = __dirname + '/tmp/dir';
automaton.run('mkdir', {
dirs: dirname
}, function (err) {
if (err) {
throw err;
}
expect(isDir(dirname)).to.be(true);
done();
});
});
it('should run a single subtask', function (done) {
var called = false;
automaton.run({
tasks: [
{
task: 'callback',
options: {
callback: function () {
called = true;
}
}
}
]
}, null, function (err) {
if (err) {
throw err;
}
expect(called).to.be(true);
done();
});
});
it('should run multiple subtasks, and different options, by the correct order', function (done) {
var stack = [];
automaton.run({
tasks: [
{
task: 'callback',
options: {
callback: function () {
stack.push(1);
}
}
},
{
task: 'callback',
options: {
callback: function () {
stack.push(2);
}
}
},
{
task: 'callback',
options: {
callback: function () {
stack.push(3);
}
}
}
]
}, null, function (err) {
if (err) {
throw err;
}
expect(stack).to.eql([1, 2, 3]);
done();
});
});
it('should run inline subtasks', function (done) {
var dirname = __dirname + '/tmp/dir';
automaton.run({
tasks: [
{
task: function (opt, ctx, next) {
fs.mkdir(dirname, parseInt('0777', 8), next);
}
}
]
}, null, function (err) {
if (err) {
throw err;
}
expect(isDir(dirname)).to.be(true);
done();
});
});
it('should run deep tasks specified directly in tasks inside tasks (not by id)', function (done) {
var stack = [];
automaton.run({
tasks: [
{
task: {
tasks: [
{
task: 'callback',
options: {
callback: function () {
stack.push(1);
}
}
}
]
}
},
{
task: 'callback',
options: {
callback: function () {
stack.push(2);
}
}
}
]
}, null, function (err) {
if (err) {
throw err;
}
expect(stack).to.eql([1, 2]);
done();
});
});
// test callback
it('should call the callback once done running a task', function (done) {
automaton.run({
tasks: [
{
task: 'callback'
}
]
}, null, function () {
done();
});
});
it('should pass an error (without colors) to the callback if there was an one', function (done) {
var assert = function (err) {
expect(err).to.be.an(Error);
expect(err.message).to.equal('wtf');
expect(removeColors(err.message) === err.message).to.equal(true);
};
async.series({
first: function (next) {
automaton.run({
tasks: [
{
task: function (opt, ctx, next) {
next(new Error('wtf'));
}
}
]
}, null, function (err) {
assert(err);
next();
});
},
second: function (next) {
automaton.run({
tasks: [
{
task: function (opt, ctx, next) {
next('wtf');
}
}
]
}, null, function (err) {
assert(err);
next();
});
},
third: function (next) {
automaton.run({
tasks: [
{
task: 'failing-task',
options: {
setup: true,
message: 'wtf'
}
}
]
}, null, function (err) {
assert(err);
next();
});
},
forth: function (next) {
automaton.run({
tasks: [
{
task: 'failing-task',
options: {
teardown: true,
message: 'wtf'
}
}
]
}, null, function (err) {
assert(err);
next();
});
},
fifth: function (next) {
automaton.run({
tasks: [
{
task: 'failing-task',
options: {
message: 'wtf'
}
}
]
}, null, function (err) {
assert(err);
next();
});
}
}, done);
});
// test if options are shared
it('should run tasks in an isolated way', function (done) {
var shared;
var counter = 0;
shared = {
task: 'callback',
options: {
callback: '{{test}}'
}
};
automaton.run({
tasks: [shared]
}, { test: function () { counter++; }}, function (err) {
if (err) {
throw err;
}
automaton.run({
tasks: [shared]
}, { test: function () { }}, function (err) {
if (err) {
throw err;
}
expect(counter).to.equal(1);
done();
});
});
});
// test "on" field
it('should skip a subtask when its "on" attribute has a falsy placeholder', function (done) {
var stack = [];
automaton.run({
setup: function (opt, ctx, next) {
opt.truthy2 = true;
next();
},
tasks: [
{
task: 'callback',
options: {
callback: function () {
stack.push(1);
}
}
},
{
task: function () {
stack.push(2);
},
on: '{{falsy1}}'
},
{
task: 'callback',
on: '{{falsy1}}',
options: {
callback: function () {
stack.push(2);
}
}
},
{
task: 'callback',
on: '{{falsy2}}',
options: {
callback: function () {
stack.push(2);
}
}
},
{
task: 'callback',
on: '{{falsy3}}',
options: {
callback: function () {
stack.push(2);
}
}
},
{
task: 'callback',
on: '{{non_existent}}',
options: {
callback: function () {
stack.push(4);
}
}
},
{
task: 'callback',
on: '{{truthy1}}',
options: {
callback: function () {
stack.push(3);
}
}
},
{
task: 'callback',
on: '{{truthy2}}',
options: {
callback: function () {
stack.push(4);
}
}
}
]
}, { falsy1: false, falsy2: undefined, falsy3: null, truthy1: 'foo' }, function (err) {
if (err) {
throw err;
}
expect(stack).to.eql([1, 3, 4]);
done();
});
});
it('should skip a task when its "on" attribute is falsy', function (done) {
var stack = [];
automaton.run({
tasks: [
{
task: 'callback',
options: {
callback: function () {
stack.push(1);
}
}
},
{
task: 'callback',
on: false,
options: {
callback: function () {
stack.push(2);
}
}
},
{
task: 'callback',
on: undefined,
options: {
callback: function () {
stack.push(2);
}
}
},
{
task: 'callback',
on: null,
options: {
callback: function () {
stack.push(2);
}
}
},
{
task: 'callback',
on: null,
options: {
callback: function () {
stack.push(2);
}
}
},
{
task: 'callback',
on: function (opts, ctx) {
expect(opts).to.be.an('object');
expect(ctx).to.be.an('object');
expect(ctx.log).to.be.an('object');
expect(ctx).to.equal(this);
expect(opts.foo).to.equal(false);
return opts.foo;
},
options: {
callback: function () {
stack.push(2);
}
}
},
{
task: 'callback',
on: true,
options: {
callback: function () {
stack.push(3);
}
}
},
{
task: 'callback',
on: [],
options: {
callback: function () {
stack.push(4);
}
}
},
{
task: 'callback',
on: function () { return true; },
options: {
callback: function () {
stack.push(5);
}
}
}
]
}, { foo: false }, function (err) {
if (err) {
throw err;
}
expect(stack).to.eql([1, 3, 4, 5]);
done();
});
});
// test required options
it('should throw if all required task options have not been passed', function (done) {
automaton.run({
tasks: [
{
task: 'mkdir'
}
]
}, null, function (err) {
expect(err).to.be.an(Error);
expect(err.message).to.match(/missing/i);
done();
});
});
// test default options
it('should assume default task options if absent', function (done) {
automaton.run({
options: {
foo: {
default: 'bar'
}
},
setup: function (opt, ctx, next) {
expect(opt.foo).to.equal('bar');
next();
},
tasks: []
}, null, function (err) {
if (err) {
throw err;
}
automaton.run({
options: {
foo: {
default: 'bar'
}
},
setup: function (opt, ctx, next) {
expect(opt.foo).to.equal('baz');
next();
},
tasks: []
}, { foo: 'baz' }, function (err) {
if (err) {
throw err;
}
done();
});
});
});
// test options replacement
it('should replace placeholders in task options', function (done) {
var someObj = { foo: 'bar' };
var opts = {
opt1: 'x',
opt2: 'y',
opt3: true,
opt4: someObj
};
automaton.run({
tasks: [
{
task: 'callback',
options: {
foo: '{{opt1}}-{{opt2}}',
bar: '{{opt3}}',
baz: '{{opt4}}',
arr: ['{{opt4}}', 'foo'],
obj: {
'{{opt1}}': '{{opt4}}',
'{{opt2}}': '{{opt1}}-{{opt2}}'
},
deep: '{{opt4.foo}}',
callback: function (opt) {
expect(opt.foo).to.be.equal('x-y');
expect(opt.bar === true).to.be.equal(true);
expect(opt.baz.foo === someObj.foo).to.be.equal(true);
expect(opt.arr[0].foo === someObj.foo).to.be.equal(true);
expect(opt.obj.x.foo === someObj.foo).to.be.equal(true);
expect(opt.obj.y).to.be.equal('x-y');
expect(opt.deep).to.be.equal('bar');
}
}
},
{
task: {
tasks: [
{
task: 'callback',
options: {
foo: '{{opt1}}-{{opt2}}',
bar: '{{opt3}}',
baz: '{{opt4}}',
arr: ['{{opt4}}', 'foo'],
obj: {
'{{opt1}}': '{{opt4}}',
'{{opt2}}': '{{opt1}}-{{opt2}}'
},
deep: '{{opt4.foo}}',
callback: function (opt) {
expect(opt.foo).to.be.equal('x-y');
expect(opt.bar === true).to.be.equal(true);
expect(opt.baz.foo === someObj.foo).to.be.equal(true);
expect(opt.arr[0].foo === someObj.foo).to.be.equal(true);
expect(opt.obj.x.foo === someObj.foo).to.be.equal(true);
expect(opt.obj.y).to.be.equal('x-y');
expect(opt.deep).to.be.equal('bar');
}
}
}
]
},
options: opts
}
]
}, opts, function (err) {
if (err) {
throw err;
}
done();
});
});
it('should replace placeholders in task options (with dot/not operator) ', function (done) {
automaton.run({
tasks: [
{
task: 'callback',
options: {
opt1: '{{truthy}}',
opt2: '{{!truthy}}',
opt3: '{{!!truthy}}',
opt4: '{{!foo}}',
opt5: '{{!!foo}}',
opt6: '{{truthy}}{{foo}}', // should not replace on this one
opt7: '{{!!!!truthy}}',
opt8: '{{!!!!truthy!!}}',
opt9: '{{!obj.foo}}',
opt10: '{{!!obj.foo}}',
opt11: '{{!!!!obj.foo}}',
opt12: '{{!!obj.foo!!}}', // should not replace on this one
callback: function (opt) {
expect(opt.opt1).to.be.equal(true);
expect(opt.opt2).to.be.equal(false);
expect(opt.opt3).to.be.equal(true);
expect(opt.opt4).to.be.equal(false);
expect(opt.opt5).to.be.equal(true);
expect(opt.opt6).to.be.equal('truebar');
expect(opt.opt7).to.be.equal(true);
expect(opt.opt8).to.be.equal('{{!!!!truthy!!}}');
expect(opt.opt9).to.be.equal(false);
expect(opt.opt10).to.be.equal(true);
expect(opt.opt11).to.be.equal(true);
expect(opt.opt12).to.be.equal('{{!!obj.foo!!}}');
}
}
}
]
}, { truthy: true, foo: 'bar', obj: { foo: 'bar' } }, function (err) {
if (err) {
throw err;
}
done();
});
});
it('should ignore escaped placeholders in task options', function (done) {
automaton.run({
tasks: [
{
task: 'callback',
options: {
someOption: '\\{\\{foo\\}\\}',
setupCallback: function (opt) {
expect(opt.someOption).to.equal('\\{\\{foo\\}\\}');
},
callback: function (opt) {
expect(opt.someOption).to.equal('{{foo}}');
},
teardownCallback: function (opt) {
expect(opt.someOption).to.equal('{{foo}}');
}
}
}
]
}, null, function (err) {
if (err) {
throw err;
}
done();
});
});
it('should execute setup before their respective tasks', function (done) {
var setup = false;
var wrong = false;
automaton.run({
tasks: [
{
task: 'callback',
options: {
setupCallback: function () {
setup = true;
},
callback: function () {
if (!setup) {
wrong = true;
}
}
}
}
]
}, null, function (err) {
if (err) {
throw err;
}
if (!setup || wrong) {
throw new Error('Setup not called or called after task');
}
done();
});
});
it('should execute teardown after their respective tasks', function (done) {
var callback = false;
var wrong = false;
automaton.run({
tasks: [
{
task: 'callback',
options: {
teardownCallback: function () {
if (!callback) {
wrong = true;
}
},
callback: function () {
callback = true;
}
}
}
]
}, null, function (err) {
if (err) {
throw err;
}
if (!callback || wrong) {
throw new Error('Teardown not called');
}
done();
});
});
it('should execute teardown even if the task failed', function (done) {
var teardown = false;
automaton.run({
tasks: [
{
task: {
teardown: function (opt, ctx, next) {
teardown = true;
next(new Error('wtf'));
},
tasks: [
{
task: function (opt, ctx, next) {
next(new Error('bleh'));
}
}
]
}
}
]
}, null, function (err) {
expect(err).to.be.an(Error);
expect(err.message).to.equal('bleh');
expect(teardown).to.equal(true);
done();
});
});
it('should let setup modify options and infer new ones', function (done) {
automaton.run({
setup: function (opt, ctx, next) {
opt.very = 'awesome';
opt.ultra = 'awesome';
next();
},
tasks: [
{
task: 'callback',
options: {
ultra: '{{ultra}}',
very: '{{very}}',
setupCallback: function (opt) {
opt.foo = 'bar';
opt.someOption = 'baz';
},
callback: function (opt) {
expect(opt.foo).to.equal('bar');
expect(opt.someOption).to.equal('baz');
expect(opt.very).to.be.equal('awesome');
expect(opt.ultra).to.be.equal('awesome');
}
}
}
]
}, { ultra: 'cool' }, function (err) {
if (err) {
throw err;
}
done();
});
});
it('should bypass tasks that fail if "fatal" attribute is falsy', function (done) {
var ok = false;
automaton.run({
tasks: [
{
task: 'failing-task',
fatal: false,
options: {
message: 'first'
}
},
{
task: 'failing-task',
fatal: false,
options: {
setup: true,
message: 'second'
}
},
{
task: 'failing-task',
fatal: '{{foo}}',
options: {
message: 'third'
}
},
{
task: 'failing-task',
fatal: function (err, opts, ctx) {
expect(err).to.be.an(Error);
expect(opts).to.be.an('object');
expect(ctx).to.be.an('object');
expect(ctx.log).to.be.an('object');
expect(ctx).to.equal(this);
expect(opts.foo).to.equal(false);
return opts.foo;
},
options: {
message: 'forth'
}
},
{
task: 'failing-task',
fatal: false,
options: {
message: 'fifth',
teardown: true
}
},
{
task: function (opts, ctx, next) {
next(new Error('bleh'));
},
fatal: false
},
{
task: function (opts, ctx, next) {
ok = true;
next();
}
},
{
task: 'failing-task',
fatal: '{{bar}}',
options: {
message: 'sixth'
}
}
]
}, { foo: false, bar: true }, function (err) {
expect(err).to.be.an(Error);
expect(err.message).to.equal('sixth');
expect(ok).to.be.ok();
done();
});
});
it('should stop the task if an error occurs in the setup even if "fatal" is false', function (next) {
var run = false;
automaton.run({
tasks: [
{
task: {
setup: function (opts, ctx, next) {
next(new Error('bleh'));
},
tasks: [
{
task: function (opts, ctx, next) {
run = true;
next();
}
}
]
},
fatal: false
}
]
}, null, function (err) {
if (err) {
throw err;
}
expect(run).to.equal(false);
next();
});
});
describe('context', function () {
it('should offer a logging interface for tasks to report', function (done) {
var assert = function (ctx) {
expect(ctx.log).to.be.ok();
expect(ctx.log.info).to.be.a('function');
expect(ctx.log.infoln).to.be.a('function');
expect(ctx.log.warn).to.be.a('function');
expect(ctx.log.warnln).to.be.a('function');
expect(ctx.log.error).to.be.a('function');
expect(ctx.log.errorln).to.be.a('function');
expect(ctx.log.success).to.be.a('function');
expect(ctx.log.successln).to.be.a('function');
};
automaton.run({
setup: function (opt, ctx, next) {
assert(this);
next();
},
tasks: [
{
task: 'callback',
options: {
setupCallback: function (opt, ctx) {
assert(this);
assert(ctx);
},
teardownCallback: function (opt, ctx) {
assert(this);
assert(ctx);
},
callback: function (opt, ctx) {
assert(this);
assert(ctx);
}
}
}
]
}, null, function (err) {
if (err) {
throw err;
}
done();
});
});
});
});
};