From f2b8b57e84ae1a0cc03cc226b430fcacc6b17f0a Mon Sep 17 00:00:00 2001 From: Kristian Kraljic Date: Mon, 23 Aug 2021 13:27:44 +0200 Subject: [PATCH] [FEATURE] Introduce concept of task identifiers to AbstractBuilder Task identifiers (in short taskIds) can now be used in AbstractBuilder to define the order of multiple task executions. So far the name of the task (taskName) was treated the identifier of a task. If the same task was added to the execution multiple times, an ID in form taskName--1 was generated, counting upwards from one. In the task definition there was no way of defining the sub-order of multiple executions. Now with the introduction of task identifiers, one can define a { id: "taskA", ... } in the task definition or call addTask with both an identifier and the name of the task to call. The old logic of generating an identifier, in case only a name is defined stays in place. The before/afterTask declarations now no longer refer to task names but to task identifiers instead. However due to task names have previously been treated as task identifiers exclusively and also the old incrementation logic for task names stayed the same, this change is compatible with any old configuration. --- lib/types/AbstractBuilder.js | 103 ++++++++------ test/lib/types/AbstractBuilder.js | 218 ++++++++++++++++++++++++++++++ 2 files changed, 283 insertions(+), 38 deletions(-) diff --git a/lib/types/AbstractBuilder.js b/lib/types/AbstractBuilder.js index 49de26847..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,48 +178,64 @@ 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 allTasks = this.taskExecutionOrder.filter((taskName) => this.hasTask(taskName) && - tasksToRun.includes(taskName.replace(/--\d+$/g, String()))); + const taskIdsToRun = tasksToRun.reduce((tasks, taskName) => { + if (this.taskExecutions[taskName]) { + tasks.push(...this.taskExecutions[taskName]); + } + + return tasks; + }, []); + + const allTasks = this.taskExecutionOrder.filter((taskId) => + this.hasTask(taskId) && taskIdsToRun.includes(taskId)); this.taskLog.addWork(allTasks.length); - return allTasks.reduce((taskChain, taskName) => { - const taskFunction = this.tasks[taskName]; + return allTasks.reduce((taskChain, taskId) => { + const taskFunction = this.tasks[taskId]; if (typeof taskFunction === "function") { - taskChain = taskChain.then(this.wrapTask(taskName, taskFunction)); + taskChain = taskChain.then(this.wrapTask(taskId, taskFunction)); } return taskChain; @@ -219,13 +246,13 @@ class AbstractBuilder { * 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 e30ceea0a..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,74 @@ 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({ @@ -353,6 +427,133 @@ test.serial("Instantiation with custom task defined three times: Custom tasks ge }); }); +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({ @@ -506,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"); }); @@ -522,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});