diff --git a/lib/types/AbstractBuilder.js b/lib/types/AbstractBuilder.js index 994123849..670ba7e8f 100644 --- a/lib/types/AbstractBuilder.js +++ b/lib/types/AbstractBuilder.js @@ -34,6 +34,7 @@ class AbstractBuilder { this.taskLog = this.log.createTaskLogger("🔨"); this.tasks = {}; + this.taskExecutions = {}; this.taskExecutionOrder = []; this.addStandardTasks({ resourceCollections, @@ -85,25 +86,34 @@ class AbstractBuilder { `at index ${i}`); } if (taskDef.beforeTask && taskDef.afterTask) { - throw new Error(`Custom task definition ${taskDef.name} of project ${project.metadata.name} ` + - `defines both "beforeTask" and "afterTask" parameters. Only one must be defined.`); + throw new Error(`Custom task definition ${taskDef.id || taskDef.name} of project ` + + `${project.metadata.name} defines both "beforeTask" and "afterTask" parameters. ` + + `Only one must be defined.`); } if (this.taskExecutionOrder.length && !taskDef.beforeTask && !taskDef.afterTask) { - // Iff there are tasks configured, beforeTask or afterTask must be given - throw new Error(`Custom task definition ${taskDef.name} of project ${project.metadata.name} ` + - `defines neither a "beforeTask" nor an "afterTask" parameter. One must be defined.`); + // If there are tasks configured, beforeTask or afterTask must be given + throw new Error(`Custom task definition ${taskDef.id || taskDef.name} of project ` + + `${project.metadata.name} defines neither a "beforeTask" nor an "afterTask" parameter. ` + + `One must be defined.`); } - let newTaskName = taskDef.name; - if (this.tasks[newTaskName]) { - // Task is already known - // => add a suffix to allow for multiple configurations of the same task - let suffixCounter = 0; - while (this.tasks[newTaskName]) { - suffixCounter++; // Start at 1 - newTaskName = `${taskDef.name}--${suffixCounter}`; + let taskId = taskDef.id; + if (!taskId) { + // No identifier defined, use the task name and add a suffix if necessary + taskId = taskDef.name; + if (this.tasks[taskId]) { + // Task is already known => add a suffix to allow for multiple configurations of the same task + let suffixCounter = 0; + while (this.tasks[taskId]) { + suffixCounter++; // Start at 1 + taskId = `${taskDef.name}--${suffixCounter}`; + } } + } else if (this.tasks[taskId]) { + throw new Error(`Conflicting custom task definition ${taskId} of project ${project.metadata.name}, ` + + `more than one task with the same identifier defined. Task identifiers must be unique.`); } + // Create custom task if not already done (task might be referenced multiple times, first one wins) const {specVersion, task} = taskRepository.getTask(taskDef.name); const execTask = function() { @@ -140,24 +150,25 @@ class AbstractBuilder { return task(params); }; - this.tasks[newTaskName] = execTask; + this.tasks[taskId] = execTask; + (this.taskExecutions[taskDef.name] || (this.taskExecutions[taskDef.name] = [])).push(taskId); if (this.taskExecutionOrder.length) { // There is at least one task configured. Use before- and afterTask to add the custom task - const refTaskName = taskDef.beforeTask || taskDef.afterTask; - let refTaskIdx = this.taskExecutionOrder.indexOf(refTaskName); + const refTaskId = taskDef.beforeTask || taskDef.afterTask; + let refTaskIdx = this.taskExecutionOrder.indexOf(refTaskId); if (refTaskIdx === -1) { - throw new Error(`Could not find task ${refTaskName}, referenced by custom task ${newTaskName}, ` + + throw new Error(`Could not find task ${refTaskId}, referenced by custom task ${taskId}, ` + `to be scheduled for project ${project.metadata.name}`); } if (taskDef.afterTask) { // Insert after index of referenced task refTaskIdx++; } - this.taskExecutionOrder.splice(refTaskIdx, 0, newTaskName); + this.taskExecutionOrder.splice(refTaskIdx, 0, taskId); } else { // There is no task configured so far. Just add the custom task - this.taskExecutionOrder.push(newTaskName); + this.taskExecutionOrder.push(taskId); } } } @@ -167,67 +178,81 @@ class AbstractBuilder { * * The order this function is being called defines the build order. FIFO. * + * @param {string} [taskId] Identifier of the task which should be in the list availableTasks. * @param {string} taskName Name of the task which should be in the list availableTasks. * @param {Function} taskFunction */ - addTask(taskName, taskFunction) { - if (this.tasks[taskName]) { - throw new Error(`Failed to add duplicate task ${taskName} for project ${this.project.metadata.name}`); + addTask(taskId, taskName, taskFunction) { + if (typeof taskName === "function") { + taskFunction = taskName; + taskName = taskId; + } + + if (this.tasks[taskId]) { + throw new Error(`Failed to add duplicate task ${taskId} for project ${this.project.metadata.name}`); } - if (this.taskExecutionOrder.includes(taskName)) { - throw new Error(`Builder: Failed to add task ${taskName} for project ${this.project.metadata.name}. ` + + if (this.taskExecutionOrder.includes(taskId)) { + throw new Error(`Builder: Failed to add task ${taskId} for project ${this.project.metadata.name}. ` + `It has already been scheduled for execution.`); } - this.tasks[taskName] = taskFunction; - this.taskExecutionOrder.push(taskName); + + this.tasks[taskId] = taskFunction; + (this.taskExecutions[taskName] || (this.taskExecutions[taskName] = [])).push(taskId); + this.taskExecutionOrder.push(taskId); } /** * Check whether a task is defined * * @private - * @param {string} taskName + * @param {string} taskId * @returns {boolean} */ - hasTask(taskName) { - return Object.prototype.hasOwnProperty.call(this.tasks, taskName); + hasTask(taskId) { + return Object.prototype.hasOwnProperty.call(this.tasks, taskId); } /** * Takes a list of tasks which should be executed from the available task list of the current builder * - * @param {Array} tasksToRun List of tasks which should be executed + * @param {Array} tasksToRun List of tasks names which should be executed * @returns {Promise} Returns promise chain with tasks */ build(tasksToRun) { - const allTasksCount = tasksToRun.filter((value) => this.hasTask(value)).length; - this.taskLog.addWork(allTasksCount); + const taskIdsToRun = tasksToRun.reduce((tasks, taskName) => { + if (this.taskExecutions[taskName]) { + tasks.push(...this.taskExecutions[taskName]); + } - let taskChain = Promise.resolve(); - for (let i = 0; i < this.taskExecutionOrder.length; i++) { - const taskName = this.taskExecutionOrder[i]; - if (this.hasTask(taskName) && tasksToRun.includes(taskName)) { - const taskFunction = this.tasks[taskName]; + return tasks; + }, []); - if (typeof taskFunction === "function") { - taskChain = taskChain.then(this.wrapTask(taskName, taskFunction)); - } + const allTasks = this.taskExecutionOrder.filter((taskId) => + this.hasTask(taskId) && taskIdsToRun.includes(taskId)); + this.taskLog.addWork(allTasks.length); + + return allTasks.reduce((taskChain, taskId) => { + const taskFunction = this.tasks[taskId]; + + if (typeof taskFunction === "function") { + taskChain = taskChain.then(this.wrapTask(taskId, taskFunction)); } - } - return taskChain; + + return taskChain; + }, Promise.resolve()); } /** * Adds progress related functionality to task function. * * @private - * @param {string} taskName Name of the task + * @param {string} taskId Name of the task * @param {Function} taskFunction Function which executed the task * @returns {Function} Wrapped task function */ - wrapTask(taskName, taskFunction) { + wrapTask(taskId, taskFunction) { return () => { - this.taskLog.startWork(`Running task ${taskName}...`); + this.taskLog.startWork(`Running task ${taskId}...`); return taskFunction().then(() => this.taskLog.completeWork(1)); }; } diff --git a/test/lib/types/AbstractBuilder.js b/test/lib/types/AbstractBuilder.js index ee5ec2585..5d87d9a94 100644 --- a/test/lib/types/AbstractBuilder.js +++ b/test/lib/types/AbstractBuilder.js @@ -91,6 +91,10 @@ test("Instantiation with standard tasks only", (t) => { const project = clone(applicationBTree); const customBuilder = new CustomBuilder({project}); + t.deepEqual(customBuilder.taskExecutions, { + "myStandardTask": ["myStandardTask"], "createDebugFiles": ["createDebugFiles"], + "replaceVersion": ["replaceVersion"] + }, "Task executions are correct"); t.deepEqual(customBuilder.taskExecutionOrder, ["myStandardTask", "createDebugFiles", "replaceVersion"], "Order of tasks is correct"); @@ -185,6 +189,10 @@ test.serial("Instantiation with custom task", (t) => { }; const customBuilder = new CustomBuilder({project}); t.truthy(customBuilder.tasks["myTask"], "Custom task has been added to task array"); + t.deepEqual(customBuilder.taskExecutions, { + "myStandardTask": ["myStandardTask"], "createDebugFiles": ["createDebugFiles"], + "myTask": ["myTask"], "replaceVersion": ["replaceVersion"] + }, "Task executions are correct"); t.deepEqual(customBuilder.taskExecutionOrder, ["myStandardTask", "createDebugFiles", "myTask", "replaceVersion"], "Order of tasks is correct"); @@ -208,6 +216,9 @@ test.serial("Instantiation of empty builder with custom tasks", (t) => { const customBuilder = new EmptyBuilder({project}); t.truthy(customBuilder.tasks["myTask"], "Custom task has been added to task array"); t.truthy(customBuilder.tasks["myTask2"], "Custom task 2 has been added to task array"); + t.deepEqual(customBuilder.taskExecutions, { + "myTask": ["myTask"], "myTask2": ["myTask2"] + }, "Task executions are correct"); t.deepEqual(customBuilder.taskExecutionOrder, ["myTask2", "myTask"], "Order of tasks is correct"); @@ -298,11 +309,251 @@ test.serial("Instantiation with custom task defined three times", (t) => { t.truthy(customBuilder.tasks["myTask"], "Custom task myTask has been added to task array"); t.truthy(customBuilder.tasks["myTask--1"], "Custom task myTask--1 has been added to task array"); t.truthy(customBuilder.tasks["myTask--2"], "Custom task myTask--2 has been added to task array"); + t.deepEqual(customBuilder.taskExecutions, { + "myStandardTask": ["myStandardTask"], "createDebugFiles": ["createDebugFiles"], + "myTask": ["myTask", "myTask--1", "myTask--2"], "replaceVersion": ["replaceVersion"] + }, "Task executions are correct"); t.deepEqual(customBuilder.taskExecutionOrder, ["myTask", "myTask--2", "myStandardTask", "createDebugFiles", "replaceVersion", "myTask--1"], "Order of tasks is correct"); }); +test.serial("Instantiation with custom task defined three times with ids", (t) => { + sinon.stub(taskRepository, "getTask").returns({ + task: function() {}, + specVersion: "2.0" + }); + + const project = clone(applicationBTree); + project.builder = { + customTasks: [{ + id: "myTask1", + name: "myTask", + beforeTask: "myStandardTask" + }, { + id: "myTask2", + name: "myTask", + afterTask: "replaceVersion" + }, { + id: "myTask3", + name: "myTask", + beforeTask: "myStandardTask" + }] + }; + const customBuilder = new CustomBuilder({project}); + t.truthy(customBuilder.tasks["myTask1"], "Custom task myTask1 has been added to task array"); + t.truthy(customBuilder.tasks["myTask2"], "Custom task myTask2 has been added to task array"); + t.truthy(customBuilder.tasks["myTask3"], "Custom task myTask3 has been added to task array"); + t.deepEqual(customBuilder.taskExecutions, { + "myStandardTask": ["myStandardTask"], "createDebugFiles": ["createDebugFiles"], + "myTask": ["myTask1", "myTask2", "myTask3"], "replaceVersion": ["replaceVersion"] + }, "Task executions are correct"); + t.deepEqual(customBuilder.taskExecutionOrder, + ["myTask1", "myTask3", "myStandardTask", "createDebugFiles", "replaceVersion", "myTask2"], + "Order of tasks is correct"); +}); + +test.serial("Instantiation of empty builder with duplicate task ids", (t) => { + sinon.stub(taskRepository, "getTask").returns({ + task: function() {}, + specVersion: "2.0" + }); + + const project = clone(applicationBTree); + project.builder = { + customTasks: [{ + id: "myTask", + name: "myTask" + }, { + id: "myTask", + name: "myTask", + afterTask: "myTask" + }] + }; + const error = t.throws(() => { + new EmptyBuilder({project}); + }); + t.deepEqual(error.message, `Conflicting custom task definition myTask of project application.b, more than ` + + `one task with the same identifier defined. Task identifiers must be unique.`, "Correct exception thrown"); +}); + +test.serial("Instantiation with custom task defined three times: Custom tasks get called correctly", async (t) => { + const customTaskStub = sinon.stub().returns(Promise.resolve()); + sinon.stub(taskRepository, "getTask").returns({ + task: customTaskStub, + specVersion: "2.0" + }); + + const project = clone(applicationBTree); + project.builder = { + customTasks: [{ + name: "myTask", + beforeTask: "myStandardTask", + configuration: "foo" + }, { + name: "myTask", + afterTask: "replaceVersion", + configuration: "bar" + }, { + name: "myTask", + beforeTask: "myStandardTask", + configuration: "baz" + }] + }; + + const resourceCollections = { + workspace: "myWorkspace", + dependencies: "myDependencies" + }; + const getInterfaceStub = sinon.stub().returns(undefined); + const taskUtil = { + getInterface: getInterfaceStub + }; + const customBuilder = new CustomBuilder({project, resourceCollections, taskUtil}); + await customBuilder.build(["myTask"]); + + t.is(getInterfaceStub.callCount, 3, "taskUtil.getInterface got called three times"); + t.is(customTaskStub.callCount, 3, "Custom task got called three times"); + ["foo", "baz", "bar"].forEach((configuration, index) => { + t.deepEqual(customTaskStub.getCall(index).args[0], { + options: { + projectName: "application.b", + projectNamespace: "application/b", + configuration + }, + workspace: "myWorkspace", + dependencies: "myDependencies" + }, `Custom task invocation ${index} got called with expected options`); + }); +}); + +test.serial("Instantiation with custom task defined three times: Custom tasks get called in right order", async (t) => { + const customTaskStub = sinon.stub().returns(Promise.resolve()); + sinon.stub(taskRepository, "getTask").returns({ + task: customTaskStub, + specVersion: "2.0" + }); + + const project = clone(applicationBTree); + project.builder = { + customTasks: [{ + id: "myTaskA", + name: "myTask", + beforeTask: "myStandardTask", + configuration: "foo" + }, { + id: "myTaskB", + name: "myTask", + afterTask: "myTaskA", + configuration: "bar" + }, { + id: "myTaskC", + name: "myTask", + beforeTask: "myTaskA", + configuration: "baz" + }] + }; + + const resourceCollections = { + workspace: "myWorkspace", + dependencies: "myDependencies" + }; + const getInterfaceStub = sinon.stub().returns(undefined); + const taskUtil = { + getInterface: getInterfaceStub + }; + const customBuilder = new CustomBuilder({project, resourceCollections, taskUtil}); + await customBuilder.build(["myTask"]); + + t.is(getInterfaceStub.callCount, 3, "taskUtil.getInterface got called three times"); + t.is(customTaskStub.callCount, 3, "Custom task got called three times"); + ["baz", "foo", "bar"].forEach((configuration, index) => { + t.deepEqual(customTaskStub.getCall(index).args[0], { + options: { + projectName: "application.b", + projectNamespace: "application/b", + configuration + }, + workspace: "myWorkspace", + dependencies: "myDependencies" + }, `Custom task invocation ${index} got called with expected options`); + }); +}); + +test.serial("Instantiation with multiple custom task defined: Custom tasks respect tasks to run", async (t) => { + const customTaskAStub = sinon.stub().returns(Promise.resolve()); + const customTaskBStub = sinon.stub().returns(Promise.resolve()); + sinon.replace(taskRepository, "getTask", sinon.fake((taskName) => ({ + task: taskName === "myTaskA" ? customTaskAStub : customTaskBStub, + specVersion: "2.0" + }))); + + const project = clone(applicationBTree); + project.builder = { + customTasks: [{ + name: "myTaskA", + beforeTask: "myStandardTask", + configuration: "foo" + }, { + name: "myTaskB", + beforeTask: "myStandardTask", + configuration: "baz" + }, { + name: "myTaskA", + afterTask: "myTaskA", + configuration: "bar" + }, { + name: "myTaskB", + beforeTask: "myTaskB", + configuration: "qux" + }] + }; + + const resourceCollections = { + workspace: "myWorkspace", + dependencies: "myDependencies" + }; + const getInterfaceStub = sinon.stub().returns(undefined); + const taskUtil = { + getInterface: getInterfaceStub + }; + const customBuilder1 = new CustomBuilder({project, resourceCollections, taskUtil}); + await customBuilder1.build(["myTaskA"]); + + t.is(getInterfaceStub.callCount, 2, "taskUtil.getInterface got called two times"); + t.is(customTaskAStub.callCount, 2, "Custom task A got called two times"); + t.is(customTaskBStub.callCount, 0, "Custom task B got called zero times"); + ["foo", "bar"].forEach((configuration, index) => { + t.deepEqual(customTaskAStub.getCall(index).args[0], { + options: { + projectName: "application.b", + projectNamespace: "application/b", + configuration + }, + workspace: "myWorkspace", + dependencies: "myDependencies" + }, `Custom task invocation ${index} got called with expected options`); + }); + + const customBuilder2 = new CustomBuilder({project, resourceCollections, taskUtil}); + await customBuilder2.build(["myTaskB"]); + + t.is(getInterfaceStub.callCount, 4, "taskUtil.getInterface got called two more times"); + t.is(customTaskAStub.callCount, 2, "Custom task A didn't get called my more times"); + t.is(customTaskBStub.callCount, 2, "Custom task B got called two times"); + ["qux", "baz"].forEach((configuration, index) => { + t.deepEqual(customTaskBStub.getCall(index).args[0], { + options: { + projectName: "application.b", + projectNamespace: "application/b", + configuration + }, + workspace: "myWorkspace", + dependencies: "myDependencies" + }, `Custom task invocation ${index} got called with expected options`); + }); +}); + test.serial("Instantiation with custom task: Custom task called correctly", (t) => { const customTaskStub = sinon.stub(); sinon.stub(taskRepository, "getTask").returns({ @@ -456,6 +707,7 @@ test("addTask: Add task", (t) => { const myFunction = function() {}; customBuilder.addTask("myTask", myFunction); t.is(customBuilder.tasks["myTask"], myFunction, "Task has been added to task array"); + t.true(customBuilder.taskExecutions.myTask.includes("myTask"), "Task executions contains task"); t.deepEqual(customBuilder.taskExecutionOrder[customBuilder.taskExecutionOrder.length - 1], "myTask", "Task has been added to end of execution order array"); }); @@ -472,6 +724,22 @@ test("addTask: Add duplicate task", (t) => { "Correct exception thrown"); }); +test("addTask: Add task with identifier", (t) => { + const project = clone(applicationBTree); + const customBuilder = new CustomBuilder({project}); + const myFunction = function() {}; + customBuilder.addTask("myTaskA", "myTask", myFunction); + customBuilder.addTask("myTaskB", "myTask", myFunction); + t.is(customBuilder.tasks["myTaskA"], myFunction, "Task A has been added to task array"); + t.is(customBuilder.tasks["myTaskB"], myFunction, "Task B has been added to task array"); + t.true(customBuilder.taskExecutions.myTask.includes("myTaskA"), "Task executions contains task A"); + t.true(customBuilder.taskExecutions.myTask.includes("myTaskB"), "Task executions contains task B"); + t.deepEqual(customBuilder.taskExecutionOrder[customBuilder.taskExecutionOrder.length - 2], "myTaskA", + "Task A has been added to end of execution order array"); + t.deepEqual(customBuilder.taskExecutionOrder[customBuilder.taskExecutionOrder.length - 1], "myTaskB", + "Task B has been added to end of execution order array"); +}); + test("addTask: Add task already added to execution order", (t) => { const project = clone(applicationBTree); const customBuilder = new CustomBuilder({project});