mirror of
https://github.com/jsdoc/jsdoc.git
synced 2025-12-08 19:46:11 +00:00
783 lines
20 KiB
JavaScript
783 lines
20 KiB
JavaScript
const Emittery = require('emittery');
|
|
const Task = require('../../../lib/task');
|
|
const TaskRunner = require('../../../lib/task-runner');
|
|
|
|
const ARGUMENT_ERROR = 'ArgumentError';
|
|
const DEPENDENCY_CYCLE_ERROR = 'DependencyCycleError';
|
|
const STATE_ERROR = 'StateError';
|
|
const UNKNOWN_DEPENDENCY_ERROR = 'UnknownDependencyError';
|
|
const UNKNOWN_TASK_ERROR = 'UnknownTaskError';
|
|
|
|
function rethrower(e) {
|
|
return () => {
|
|
throw e;
|
|
};
|
|
}
|
|
|
|
describe('@jsdoc/task-runner/lib/task-runner', () => {
|
|
let badTask;
|
|
let bar;
|
|
let barResult;
|
|
const fakeTask = {
|
|
name: 'foo',
|
|
func: () => Promise.resolve()
|
|
};
|
|
let foo;
|
|
let fooResult;
|
|
let runner;
|
|
|
|
beforeEach(() => {
|
|
runner = new TaskRunner();
|
|
foo = new Task({
|
|
name: 'foo',
|
|
func: () => new Promise(resolve => {
|
|
fooResult = true;
|
|
resolve();
|
|
})
|
|
});
|
|
fooResult = null;
|
|
bar = new Task({
|
|
name: 'bar',
|
|
func: () => new Promise(resolve => {
|
|
barResult = true;
|
|
resolve();
|
|
})
|
|
});
|
|
barResult = null;
|
|
badTask = new Task({
|
|
name: 'badTask',
|
|
func: () => Promise.reject(new Error())
|
|
});
|
|
});
|
|
|
|
it('is a function', () => {
|
|
expect(TaskRunner).toBeFunction();
|
|
});
|
|
|
|
it('inherits from emittery', () => {
|
|
expect(runner instanceof Emittery).toBeTrue();
|
|
});
|
|
|
|
it('has no required parameters', () => {
|
|
function factory() {
|
|
return new TaskRunner();
|
|
}
|
|
|
|
expect(factory).not.toThrow();
|
|
});
|
|
|
|
it('accepts a context object', () => {
|
|
const context = {};
|
|
|
|
runner = new TaskRunner(context);
|
|
|
|
expect(runner.context).toBe(context);
|
|
});
|
|
|
|
it('does not accept a non-object context', () => {
|
|
function factory() {
|
|
return new TaskRunner(7);
|
|
}
|
|
|
|
expect(factory).toThrowErrorOfType(ARGUMENT_ERROR);
|
|
});
|
|
|
|
describe('addTask', () => {
|
|
it('adds `Task` objects', () => {
|
|
function add() {
|
|
runner.addTask(foo);
|
|
}
|
|
|
|
expect(add).not.toThrow();
|
|
});
|
|
|
|
it('fails with non-`Task` objects', () => {
|
|
function add() {
|
|
runner.addTask(fakeTask);
|
|
}
|
|
|
|
expect(add).toThrowErrorOfType(ARGUMENT_ERROR);
|
|
});
|
|
|
|
it('fails if the task runner is already running', () => {
|
|
function addWhileRunning() {
|
|
runner.run();
|
|
runner.addTask(foo);
|
|
}
|
|
|
|
expect(addWhileRunning).toThrowErrorOfType(STATE_ERROR);
|
|
});
|
|
|
|
// We run the task, rather than just checking the value of `runner.tasks`, to make sure
|
|
// _all_ the internal state was set correctly.
|
|
it('correctly adds the task', async () => {
|
|
runner.addTask(foo);
|
|
await runner.run();
|
|
|
|
expect(fooResult).toBeTrue();
|
|
});
|
|
|
|
it('causes the task to be emitted when the task starts', async () => {
|
|
let promise;
|
|
|
|
runner.addTask(foo);
|
|
promise = runner.once('taskStart');
|
|
|
|
foo.run();
|
|
await promise.then(event => {
|
|
expect(event).toBe(foo);
|
|
});
|
|
});
|
|
|
|
it('causes the task to be emitted when the task ends', async () => {
|
|
let event;
|
|
let promise;
|
|
|
|
runner.addTask(foo);
|
|
promise = runner.once('taskEnd');
|
|
|
|
foo.run();
|
|
event = await promise;
|
|
|
|
expect(event).toBe(foo);
|
|
});
|
|
|
|
it('causes an error to be emitted if the task errors', async () => {
|
|
let error;
|
|
let event;
|
|
let promise;
|
|
|
|
runner.addTask(badTask);
|
|
promise = runner.once('taskError');
|
|
|
|
try {
|
|
await runner.run();
|
|
} catch (e) {
|
|
error = e;
|
|
}
|
|
|
|
event = await promise;
|
|
|
|
expect(event).toBeObject();
|
|
expect(rethrower(event.error)).toThrowError();
|
|
expect(event.error).toBe(error);
|
|
expect(event.task).toBe(badTask);
|
|
});
|
|
});
|
|
|
|
describe('addTasks', () => {
|
|
it('accepts an object whose values are tasks', () => {
|
|
function addTasks() {
|
|
runner.addTasks({ foo });
|
|
}
|
|
|
|
expect(addTasks).not.toThrow();
|
|
});
|
|
|
|
it('fails with an object whose values are not tasks', () => {
|
|
function addTasks() {
|
|
runner.addTasks({ foo: fakeTask });
|
|
}
|
|
|
|
expect(addTasks).toThrowErrorOfType(ARGUMENT_ERROR);
|
|
});
|
|
|
|
it('accepts an array of tasks', () => {
|
|
function addTasks() {
|
|
runner.addTasks([foo]);
|
|
}
|
|
|
|
expect(addTasks).not.toThrow();
|
|
});
|
|
|
|
it('fails with an array of non-tasks', () => {
|
|
function addTasks() {
|
|
runner.addTasks([fakeTask]);
|
|
}
|
|
|
|
expect(addTasks).toThrowErrorOfType(ARGUMENT_ERROR);
|
|
});
|
|
|
|
it('fails with non-object, non-array input', () => {
|
|
function addTasks() {
|
|
runner.addTasks(7);
|
|
}
|
|
|
|
expect(addTasks).toThrowErrorOfType(ARGUMENT_ERROR);
|
|
});
|
|
|
|
it('adds all the tasks in an object', () => {
|
|
let tasks;
|
|
|
|
runner.addTasks({
|
|
foo,
|
|
bar
|
|
});
|
|
tasks = runner.tasks;
|
|
|
|
expect(tasks.foo).toBe(foo);
|
|
expect(tasks.bar).toBe(bar);
|
|
});
|
|
|
|
it('adds all the tasks in an array', () => {
|
|
let tasks;
|
|
|
|
runner.addTasks([
|
|
foo,
|
|
bar
|
|
]);
|
|
tasks = runner.tasks;
|
|
|
|
expect(tasks.foo).toBe(foo);
|
|
expect(tasks.bar).toBe(bar);
|
|
});
|
|
|
|
it('returns `this`', () => {
|
|
const result = runner.addTasks({
|
|
foo,
|
|
bar
|
|
});
|
|
|
|
expect(result).toBe(runner);
|
|
});
|
|
});
|
|
|
|
describe('context', () => {
|
|
it('is the provided context', () => {
|
|
const context = {};
|
|
const r = new TaskRunner(context);
|
|
|
|
expect(r.context).toBe(context);
|
|
});
|
|
});
|
|
|
|
describe('end', () => {
|
|
it('stops the task runner', () => {
|
|
function addAfterEnding() {
|
|
runner.addTask(foo);
|
|
runner.run();
|
|
runner.end();
|
|
runner.addTask(bar);
|
|
}
|
|
|
|
expect(addAfterEnding).not.toThrow();
|
|
});
|
|
|
|
it('resets the tasks', async () => {
|
|
runner.addTask(foo);
|
|
runner.run();
|
|
await runner.end();
|
|
|
|
expect(runner.tasks).toBeEmptyObject();
|
|
});
|
|
|
|
it('resets the context', async () => {
|
|
runner.addTask(foo);
|
|
runner.run();
|
|
await runner.end();
|
|
|
|
expect(runner.context).toBeEmptyObject();
|
|
});
|
|
});
|
|
|
|
describe('removeTask', () => {
|
|
it('removes `Task` objects', () => {
|
|
runner.addTask(foo);
|
|
runner.removeTask(foo);
|
|
|
|
expect(runner.tasks.foo).toBeUndefined();
|
|
});
|
|
|
|
it('removes tasks by name', () => {
|
|
runner.addTask(foo);
|
|
runner.removeTask('foo');
|
|
|
|
expect(runner.tasks.foo).toBeUndefined();
|
|
});
|
|
|
|
it('fails on invalid input types', () => {
|
|
function addRemove() {
|
|
runner.addTask(foo);
|
|
runner.removeTask(7);
|
|
}
|
|
|
|
expect(addRemove).toThrowErrorOfType(ARGUMENT_ERROR);
|
|
});
|
|
|
|
it('fails on unknown tasks', () => {
|
|
function addRemove() {
|
|
runner.addTask(foo);
|
|
runner.removeTask(bar);
|
|
}
|
|
|
|
expect(addRemove).toThrowErrorOfType(UNKNOWN_TASK_ERROR);
|
|
});
|
|
|
|
it('fails if the task runner is already running', () => {
|
|
function removeWhileRunning() {
|
|
runner.addTasks([foo, bar]);
|
|
runner.run();
|
|
runner.removeTask(foo);
|
|
}
|
|
|
|
expect(removeWhileRunning).toThrowErrorOfType(STATE_ERROR);
|
|
});
|
|
|
|
it('correctly removes the task', async () => {
|
|
let error;
|
|
|
|
runner.addTasks([foo, bar]);
|
|
runner.removeTask(foo);
|
|
|
|
try {
|
|
await runner.run();
|
|
} catch (e) {
|
|
error = e;
|
|
}
|
|
|
|
expect(error).toBeUndefined();
|
|
expect(fooResult).toBeNull();
|
|
expect(barResult).toBeTrue();
|
|
});
|
|
|
|
it('prevents `taskStart`/`taskEnd` events for the task', async () => {
|
|
let startEvent;
|
|
let endEvent;
|
|
|
|
runner.on('taskStart', e => {
|
|
startEvent = e;
|
|
});
|
|
runner.on('taskEnd', e => {
|
|
endEvent = e;
|
|
});
|
|
runner.addTask(foo);
|
|
runner.removeTask(foo);
|
|
await foo.run();
|
|
|
|
expect(startEvent).toBeUndefined();
|
|
expect(endEvent).toBeUndefined();
|
|
});
|
|
|
|
it('prevents `taskError` events for the task', async () => {
|
|
let errorEvent;
|
|
let taskErrorEvent;
|
|
|
|
runner.addTask(badTask);
|
|
runner.removeTask(badTask);
|
|
|
|
badTask.on('error', e => {
|
|
errorEvent = e;
|
|
});
|
|
runner.on('taskError', e => {
|
|
taskErrorEvent = e;
|
|
});
|
|
|
|
try {
|
|
await badTask.run();
|
|
} catch (e) {
|
|
// Expected behavior.
|
|
}
|
|
|
|
expect(errorEvent).toBeObject();
|
|
expect(errorEvent.task).toBe(badTask);
|
|
expect(taskErrorEvent).toBeUndefined();
|
|
});
|
|
|
|
it('returns `this`', () => {
|
|
let result;
|
|
|
|
runner.addTask(foo);
|
|
result = runner.removeTask(foo);
|
|
|
|
expect(result).toBe(runner);
|
|
});
|
|
});
|
|
|
|
describe('removeTasks', () => {
|
|
it('accepts an object whose values are tasks', () => {
|
|
function removeTasks() {
|
|
runner.removeTasks({ foo });
|
|
}
|
|
|
|
runner.addTask(foo);
|
|
|
|
expect(removeTasks).not.toThrow();
|
|
});
|
|
|
|
it('fails with an object whose values are not tasks', () => {
|
|
function removeTasks() {
|
|
runner.removeTasks({ foo: 7 });
|
|
}
|
|
|
|
runner.addTask(foo);
|
|
|
|
expect(removeTasks).toThrowErrorOfType(ARGUMENT_ERROR);
|
|
});
|
|
|
|
it('accepts an array of tasks', () => {
|
|
function removeTasks() {
|
|
runner.removeTasks([foo]);
|
|
}
|
|
|
|
runner.addTask(foo);
|
|
|
|
expect(removeTasks).not.toThrow();
|
|
});
|
|
|
|
it('accepts an array of strings', () => {
|
|
function removeTasks() {
|
|
runner.removeTasks(['foo']);
|
|
}
|
|
|
|
runner.addTask(foo);
|
|
|
|
expect(removeTasks).not.toThrow();
|
|
});
|
|
|
|
it('fails with an array whose values are not tasks or strings', () => {
|
|
function removeTasks() {
|
|
runner.removeTasks([fakeTask]);
|
|
}
|
|
|
|
expect(removeTasks).toThrowErrorOfType(ARGUMENT_ERROR);
|
|
});
|
|
|
|
it('fails with non-object, non-array input', () => {
|
|
function removeTasks() {
|
|
runner.removeTasks(7);
|
|
}
|
|
|
|
expect(removeTasks).toThrowErrorOfType(ARGUMENT_ERROR);
|
|
});
|
|
|
|
it('fails with unknown tasks', () => {
|
|
function removeTasks() {
|
|
runner.removeTasks([bar]);
|
|
}
|
|
|
|
expect(removeTasks).toThrowErrorOfType(UNKNOWN_TASK_ERROR);
|
|
});
|
|
|
|
it('removes all the tasks in an object', () => {
|
|
const tasks = {
|
|
foo,
|
|
bar
|
|
};
|
|
|
|
runner.addTasks(tasks);
|
|
runner.removeTasks(tasks);
|
|
|
|
expect(runner.tasks).toBeEmptyObject();
|
|
});
|
|
|
|
it('removes all the tasks in an array', () => {
|
|
const tasks = [
|
|
foo,
|
|
bar
|
|
];
|
|
|
|
runner.addTasks(tasks);
|
|
runner.removeTasks(tasks);
|
|
|
|
expect(runner.tasks).toBeEmptyObject();
|
|
});
|
|
|
|
it('returns `this`', () => {
|
|
runner.addTask(foo);
|
|
|
|
expect(runner.removeTask(foo)).toBe(runner);
|
|
});
|
|
});
|
|
|
|
describe('run', () => {
|
|
let a;
|
|
let b;
|
|
let c;
|
|
let taskA;
|
|
let taskB;
|
|
let taskC;
|
|
|
|
beforeEach(() => {
|
|
a = null;
|
|
b = null;
|
|
c = null;
|
|
|
|
taskA = new Task({
|
|
name: 'a',
|
|
func: () => {
|
|
a = 5;
|
|
|
|
return Promise.resolve();
|
|
}
|
|
});
|
|
taskB = new Task({
|
|
name: 'b',
|
|
func: () => {
|
|
b = a * 3;
|
|
|
|
return Promise.resolve();
|
|
},
|
|
dependsOn: ['a']
|
|
});
|
|
taskC = new Task({
|
|
name: 'c',
|
|
func: () => {
|
|
c = a + b;
|
|
|
|
return Promise.resolve();
|
|
},
|
|
dependsOn: ['b']
|
|
});
|
|
});
|
|
|
|
it('runs every task', async () => {
|
|
runner.addTasks([taskA, taskB, taskC]);
|
|
await runner.run();
|
|
|
|
expect(a).toBe(5);
|
|
expect(b).toBe(15);
|
|
expect(c).toBe(20);
|
|
});
|
|
|
|
it('runs tasks with dependencies in the correct order', async () => {
|
|
const results = [];
|
|
|
|
taskA.on('end', () => {
|
|
results.push(a);
|
|
});
|
|
taskB.on('end', () => {
|
|
results.push(b);
|
|
});
|
|
taskC.on('end', () => {
|
|
results.push(c);
|
|
});
|
|
|
|
runner.addTasks([taskA, taskB, taskC]);
|
|
await runner.run();
|
|
|
|
expect(results).toEqual([5, 15, 20]);
|
|
});
|
|
|
|
it('fails if the task runner is already running', async () => {
|
|
let error;
|
|
|
|
runner.addTask(taskA);
|
|
runner.run();
|
|
|
|
try {
|
|
await runner.run();
|
|
} catch (e) {
|
|
error = e;
|
|
}
|
|
|
|
expect(rethrower(error)).toThrowErrorOfType(STATE_ERROR);
|
|
});
|
|
|
|
it('clears all of its state after it runs', async () => {
|
|
runner.addTask(taskA);
|
|
await runner.run();
|
|
|
|
expect(runner.context).toBeEmptyObject();
|
|
expect(runner.tasks).toBeEmptyObject();
|
|
});
|
|
|
|
it('fails if a task errors', async () => {
|
|
let error;
|
|
|
|
runner.addTask(badTask);
|
|
try {
|
|
await runner.run();
|
|
} catch (e) {
|
|
error = e;
|
|
}
|
|
|
|
expect(rethrower(error)).toThrowError();
|
|
});
|
|
|
|
describe('context', () => {
|
|
it('passes the context to tasks with no dependencies', async () => {
|
|
const context = {};
|
|
const r = new TaskRunner(context);
|
|
|
|
r.addTask(new Task({
|
|
name: 'usesContext',
|
|
func: ctx => {
|
|
ctx.foo = 'bar';
|
|
|
|
return Promise.resolve();
|
|
}
|
|
}));
|
|
|
|
await r.run();
|
|
expect(context.foo).toBe('bar');
|
|
});
|
|
|
|
it('passes the context to tasks with dependencies', async () => {
|
|
const context = {};
|
|
const r = new TaskRunner(context);
|
|
|
|
r.addTasks([
|
|
new Task({
|
|
name: 'one',
|
|
func: ctx => {
|
|
ctx.foo = 'bar';
|
|
|
|
return Promise.resolve();
|
|
}
|
|
}),
|
|
new Task({
|
|
name: 'two',
|
|
func: ctx => {
|
|
ctx.bar = ctx.foo + ' baz';
|
|
|
|
return Promise.resolve();
|
|
},
|
|
dependsOn: ['one']
|
|
})
|
|
]);
|
|
|
|
await r.run();
|
|
expect(context.bar).toBe('bar baz');
|
|
});
|
|
});
|
|
|
|
describe('dependencies', () => {
|
|
it('errors if a task depends on an unknown task', async () => {
|
|
let error;
|
|
|
|
runner.addTask(new Task({
|
|
name: 'badDependsOn',
|
|
func: () => Promise.resolve(),
|
|
dependsOn: ['mysteryTask']
|
|
}));
|
|
|
|
try {
|
|
await runner.run();
|
|
} catch (e) {
|
|
error = e;
|
|
}
|
|
|
|
expect(rethrower(error)).toThrowErrorOfType(UNKNOWN_DEPENDENCY_ERROR);
|
|
});
|
|
|
|
it('errors if there are circular dependencies', async () => {
|
|
let error;
|
|
|
|
runner.addTasks([
|
|
new Task({
|
|
name: 'one',
|
|
func: () => Promise.resolve(),
|
|
dependsOn: ['two']
|
|
}),
|
|
new Task({
|
|
name: 'two',
|
|
func: () => Promise.resolve(),
|
|
dependsOn: ['one']
|
|
})
|
|
]);
|
|
|
|
try {
|
|
await runner.run();
|
|
} catch (e) {
|
|
error = e;
|
|
}
|
|
|
|
expect(rethrower(error)).toThrowErrorOfType(DEPENDENCY_CYCLE_ERROR);
|
|
});
|
|
});
|
|
|
|
describe('events', () => {
|
|
it('emits a `start` event', async () => {
|
|
let emitted;
|
|
|
|
runner.addTask(foo);
|
|
runner.on('start', () => {
|
|
emitted = true;
|
|
});
|
|
await runner.run();
|
|
|
|
expect(emitted).toBeTrue();
|
|
});
|
|
|
|
it('emits an `end` event', async () => {
|
|
let emitted;
|
|
|
|
runner.addTask(foo);
|
|
runner.on('end', e => {
|
|
emitted = e;
|
|
});
|
|
await runner.run();
|
|
|
|
expect(emitted).toBeObject();
|
|
expect(emitted.error).toBeNull();
|
|
});
|
|
|
|
it('fails and emits an error in the `end` event if necessary', async () => {
|
|
let endError;
|
|
let error;
|
|
|
|
runner.addTask(badTask);
|
|
runner.on('end', e => {
|
|
endError = e.error;
|
|
});
|
|
|
|
try {
|
|
await runner.run();
|
|
} catch (e) {
|
|
error = e;
|
|
}
|
|
|
|
expect(rethrower(endError)).toThrowError();
|
|
expect(endError).toBe(error);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('running', () => {
|
|
it('is true when the task runner is running', async () => {
|
|
let running;
|
|
|
|
runner.addTask(new Task({
|
|
name: 'checkRunning',
|
|
func: () => {
|
|
running = runner.running;
|
|
|
|
return Promise.resolve();
|
|
}
|
|
}));
|
|
await runner.run();
|
|
|
|
expect(running).toBeTrue();
|
|
});
|
|
|
|
it('is false when the task runner has not started', () => {
|
|
runner.addTask(foo);
|
|
|
|
expect(runner.running).toBeFalse();
|
|
});
|
|
|
|
it('is false after the task runner has finished', async () => {
|
|
runner.addTask(foo);
|
|
await runner.run();
|
|
|
|
expect(runner.running).toBeFalse();
|
|
});
|
|
});
|
|
|
|
describe('tasks', () => {
|
|
it('is an object in which keys are task names and values are tasks', () => {
|
|
let tasks;
|
|
|
|
runner.addTask(foo);
|
|
tasks = runner.tasks;
|
|
|
|
expect(tasks).toBeObject();
|
|
expect(tasks.foo).toBe(foo);
|
|
});
|
|
|
|
it('is an empty object if no tasks have been added', () => {
|
|
expect(runner.tasks).toBeEmptyObject();
|
|
});
|
|
});
|
|
});
|