diff --git a/packages/logger/lib/loggers/ProjectBuild.js b/packages/logger/lib/loggers/ProjectBuild.js index 83f39497081..61c81115963 100644 --- a/packages/logger/lib/loggers/ProjectBuild.js +++ b/packages/logger/lib/loggers/ProjectBuild.js @@ -83,6 +83,24 @@ class ProjectBuild extends Logger { this._log(level, `${this.#projectName}: Finished task ${taskName}`); } } + + skipTask(taskName) { + if (!this.#tasksToRun || !this.#tasksToRun.includes(taskName)) { + throw new Error(`loggers/ProjectBuild#skipTask: Unknown task ${taskName}`); + } + const level = "info"; + const hasListeners = this._emit(ProjectBuild.PROJECT_BUILD_STATUS_EVENT_NAME, { + level, + projectName: this.#projectName, + projectType: this.#projectType, + taskName, + status: "task-skip", + }); + + if (!hasListeners) { + this._log(level, `${this.#projectName}: Skipping task ${taskName}`); + } + } } export default ProjectBuild; diff --git a/packages/logger/lib/writers/Console.js b/packages/logger/lib/writers/Console.js index b2fc29f1563..8b90b28567e 100644 --- a/packages/logger/lib/writers/Console.js +++ b/packages/logger/lib/writers/Console.js @@ -21,6 +21,7 @@ class Console { #progressBarContainer; #progressBar; #progressProjectWeight; + #moduleFilter; constructor() { this._handleLogEvent = this.#handleLogEvent.bind(this); @@ -29,6 +30,72 @@ class Console { this._handleBuildMetadataEvent = this.#handleBuildMetadataEvent.bind(this); this._handleProjectBuildMetadataEvent = this.#handleProjectBuildMetadataEvent.bind(this); this._handleStop = this.disable.bind(this); + this.#initFiters(); + } + + #initFiters() { + const modulesPatterns = process.env.UI5_LOG_MODULES; + if (!modulesPatterns) { + this.#moduleFilter = null; + return; + } + const enabledModules = []; + const enabledNamespaces = []; + const disabledModules = []; + const disabledNamespaces = []; + // Example of modulePattern: "module1,module2:submodule:subsubmodule,module3:*:-module3:submodule" + modulesPatterns.split(",").forEach((modulePattern) => { + const pattern = modulePattern.trim(); + if (pattern.startsWith("-")) { + if (pattern.endsWith(":*")) { + disabledNamespaces.push(pattern.substring(1, pattern.length - 2)); + } else { + disabledModules.push(pattern.substring(1)); + } + } else { + if (pattern.endsWith(":*")) { + enabledNamespaces.push(pattern.substring(0, pattern.length - 2)); + } else { + enabledModules.push(pattern); + } + } + }); + + this.#moduleFilter = { + enabledModules, + enabledNamespaces, + disabledModules, + disabledNamespaces, + }; + } + + #filterModule(moduleName) { + if (!this.#moduleFilter) { + return true; + } + if (this.#moduleFilter.disabledModules.includes(moduleName)) { + return false; + } + if (this.#moduleFilter.enabledModules.includes(moduleName)) { + return true; + } + const moduleParts = moduleName.split(":"); + for (let i = moduleParts.length - 1; i > 0; i--) { + const namespace = moduleParts.slice(0, i).join(":"); + if (this.#moduleFilter.disabledNamespaces.includes(namespace)) { + return false; + } + if (this.#moduleFilter.enabledNamespaces.includes(namespace)) { + return true; + } + } + + // If any module or namespace is enabled, all other modules are disabled by default + if (this.#moduleFilter.enabledModules.length > 0 || this.#moduleFilter.enabledNamespaces.length > 0) { + return false; + } + + return true; } /** @@ -137,7 +204,9 @@ class Console { } #handleLogEvent({level, message, moduleName}) { - this.#writeMessage(level, `${chalk.blue(moduleName)} ${message}`); + if (this.#filterModule(moduleName)) { + this.#writeMessage(level, `${chalk.blue(moduleName)} ${message}`); + } } #handleBuildMetadataEvent({projectsToBuild}) { @@ -328,6 +397,23 @@ class Console { taskMetadata.executionEnded = true; message = `${chalk.green(figures.tick)} Finished task ${chalk.bold(taskName)}`; + // Update progress bar (if used) + this._getProgressBar()?.increment(1); + break; + case "task-skip": + if (taskMetadata.executionEnded) { + throw new Error(`writers/Console: ` + + `Unexpected task-skip event for project ${projectName}, task ${taskName}. ` + + `Task execution already ended`); + } + if (taskMetadata.executionStarted) { + throw new Error(`writers/Console: ` + + `Unexpected task-skip event for project ${projectName}, task ${taskName}. ` + + `Task execution already started`); + } + taskMetadata.executionEnded = true; + message = `${chalk.green(figures.tick)} Skipping task ${chalk.bold(taskName)}`; + // Update progress bar (if used) this._getProgressBar()?.increment(1); break; diff --git a/packages/logger/test/lib/loggers/ProjectBuild.js b/packages/logger/test/lib/loggers/ProjectBuild.js index a22506cf3d9..cc8ae00d45a 100644 --- a/packages/logger/test/lib/loggers/ProjectBuild.js +++ b/packages/logger/test/lib/loggers/ProjectBuild.js @@ -228,3 +228,58 @@ test.serial("End task: Unknown task", (t) => { }, "Threw with expected error message"); }); +test.serial("Skip task", (t) => { + const {projectBuildLogger, logHandler, metadataHandler, statusHandler, logStub} = t.context; + projectBuildLogger.setTasks(["task.a"]); + + projectBuildLogger.skipTask("task.a"); + + t.is(statusHandler.callCount, 1, "One build-status event emitted"); + t.deepEqual(statusHandler.getCall(0).args[0], { + level: "info", + projectName: "projectName", + projectType: "projectType", + status: "task-skip", + taskName: "task.a", + }, "Metadata event has expected payload"); + + t.is(logHandler.callCount, 0, "No log event emitted"); + t.is(metadataHandler.callCount, 1, "One build-metadata event emitted"); + t.is(logStub.callCount, 0, "_log was never called"); +}); + +test.serial("No event listener: Skip task", (t) => { + const {projectBuildLogger, logHandler, metadataHandler, statusHandler, logStub} = t.context; + process.off(ProjectBuildLogger.PROJECT_BUILD_STATUS_EVENT_NAME, statusHandler); + projectBuildLogger.setTasks(["task.a"]); + + projectBuildLogger.skipTask("task.a"); + t.is(logStub.callCount, 1, "_log got called once"); + t.is(logStub.getCall(0).args[0], "info", "Logged with expected log-level"); + t.is(logStub.getCall(0).args[1], + "projectName: Skipping task task.a", + "Logged expected message"); + + t.is(logHandler.callCount, 0, "No log event emitted"); + t.is(metadataHandler.callCount, 1, "One build-metadata event emitted"); +}); + +test.serial("Skip task: Unknown task", (t) => { + const {projectBuildLogger} = t.context; + + // Throws because no projects are set + t.throws(() => { + projectBuildLogger.skipTask("task.x"); + }, { + message: `loggers/ProjectBuild#skipTask: Unknown task task.x` + }, "Threw with expected error message"); + + projectBuildLogger.setTasks(["task.a"]); + // Throws because given project is unknown + t.throws(() => { + projectBuildLogger.skipTask("task.x"); + }, { + message: `loggers/ProjectBuild#skipTask: Unknown task task.x` + }, "Threw with expected error message"); +}); + diff --git a/packages/logger/test/lib/writers/Console.js b/packages/logger/test/lib/writers/Console.js index 204be01d96c..3f745ba6f0b 100644 --- a/packages/logger/test/lib/writers/Console.js +++ b/packages/logger/test/lib/writers/Console.js @@ -721,6 +721,114 @@ test.serial("ProjectBuild status (end): Task execution not started", (t) => { t.is(stderrWriteStub.callCount, 0, "Logged zero messages"); }); +test.serial("ProjectBuild status (skip)", (t) => { + const {stderrWriteStub} = t.context; + process.emit("ui5.build-metadata", { + projectsToBuild: ["project.a"] + }); + + process.emit("ui5.project-build-metadata", { + projectName: "project.a", + projectType: "project-type", + tasksToRun: ["task.a"] + }); + + process.emit("ui5.project-build-status", { + level: "info", + projectName: "project.a", + projectType: "project-type", + taskName: "task.a", + status: "task-skip", + }); + + t.is(stderrWriteStub.callCount, 1, "Logged one message"); + t.is(stripAnsi(stderrWriteStub.getCall(0).args[0]), + `info project.a ${figures.tick} Skipping task task.a\n`, + "Logged expected message"); +}); + +test.serial("ProjectBuild status (skip): Task execution already started", (t) => { + const {stderrWriteStub} = t.context; + process.emit("ui5.build-metadata", { + projectsToBuild: ["project.a"] + }); + + process.emit("ui5.project-build-metadata", { + projectName: "project.a", + projectType: "project-type", + tasksToRun: ["task.a"] + }); + + process.emit("ui5.project-build-status", { + level: "silly", + projectName: "project.a", + projectType: "project-type", + taskName: "task.a", + status: "task-start", + }); + + t.throws(() => { + process.emit("ui5.project-build-status", { + level: "info", + projectName: "project.a", + projectType: "project-type", + taskName: "task.a", + status: "task-skip", + }); + }, { + message: + "writers/Console: Unexpected task-skip event for project project.a, task task.a. " + + "Task execution already started" + }); + + t.is(stderrWriteStub.callCount, 0, "Logged zero messages"); +}); + +test.serial("ProjectBuild status (skip): Task execution already ended", (t) => { + const {stderrWriteStub} = t.context; + process.emit("ui5.build-metadata", { + projectsToBuild: ["project.a"] + }); + + process.emit("ui5.project-build-metadata", { + projectName: "project.a", + projectType: "project-type", + tasksToRun: ["task.a"] + }); + + process.emit("ui5.project-build-status", { + level: "silly", + projectName: "project.a", + projectType: "project-type", + taskName: "task.a", + status: "task-start", + }); + + process.emit("ui5.project-build-status", { + level: "silly", + projectName: "project.a", + projectType: "project-type", + taskName: "task.a", + status: "task-end", + }); + + t.throws(() => { + process.emit("ui5.project-build-status", { + level: "info", + projectName: "project.a", + projectType: "project-type", + taskName: "task.a", + status: "task-skip", + }); + }, { + message: + "writers/Console: Unexpected task-skip event for project project.a, task task.a. " + + "Task execution already ended" + }); + + t.is(stderrWriteStub.callCount, 0, "Logged zero messages"); +}); + test.serial("ProjectBuild status: Unknown status", (t) => { const {stderrWriteStub} = t.context; process.emit("ui5.build-metadata", { diff --git a/packages/logger/test/lib/writers/ConsoleFilter.js b/packages/logger/test/lib/writers/ConsoleFilter.js new file mode 100644 index 00000000000..fe507289251 --- /dev/null +++ b/packages/logger/test/lib/writers/ConsoleFilter.js @@ -0,0 +1,108 @@ +import test from "ava"; +import sinon from "sinon"; +import stripAnsi from "strip-ansi"; +import ConsoleWriter from "../../../lib/writers/Console.js"; + +test.serial.beforeEach((t) => { + t.context.stderrWriteStub = sinon.stub(process.stderr, "write"); + t.context.originalIsTty = process.stderr.isTTY; + process.env.UI5_LOG_LVL = "info"; +}); + +test.serial.afterEach.always((t) => { + if (t.context.consoleWriter) { + t.context.consoleWriter.disable(); + } + sinon.restore(); + process.stderr.isTTY = t.context.originalIsTty; + delete process.env.UI5_LOG_LVL; + delete process.env.UI5_LOG_MODULES; +}); + +test.serial("Module filtering: No filter", (t) => { + const {stderrWriteStub} = t.context; + t.context.consoleWriter = ConsoleWriter.init(); + + process.emit("ui5.log", { + level: "info", + message: "Message 1", + moduleName: "my:module" + }); + + t.is(stderrWriteStub.callCount, 1, "Logged one message"); + t.is(stripAnsi(stderrWriteStub.getCall(0).args[0]), `info my:module Message 1\n`, + "Logged expected message"); +}); + +test.serial("Module filtering: Enable one module", (t) => { + const {stderrWriteStub} = t.context; + process.env.UI5_LOG_MODULES = "my:module"; + t.context.consoleWriter = ConsoleWriter.init(); + + process.emit("ui5.log", {level: "info", message: "Message 1", moduleName: "my:module"}); + process.emit("ui5.log", {level: "info", message: "Message 2", moduleName: "other:module"}); + + t.is(stderrWriteStub.callCount, 1, "Logged one message"); + t.is(stripAnsi(stderrWriteStub.getCall(0).args[0]), `info my:module Message 1\n`, + "Logged expected message"); +}); + +test.serial("Module filtering: Enable one namespace", (t) => { + const {stderrWriteStub} = t.context; + process.env.UI5_LOG_MODULES = "my:*"; + t.context.consoleWriter = ConsoleWriter.init(); + + process.emit("ui5.log", {level: "info", message: "Message 1", moduleName: "my:module"}); + process.emit("ui5.log", {level: "info", message: "Message 2", moduleName: "my:other:module"}); + process.emit("ui5.log", {level: "info", message: "Message 3", moduleName: "other:module"}); + + t.is(stderrWriteStub.callCount, 2, "Logged two messages"); + t.is(stripAnsi(stderrWriteStub.getCall(0).args[0]), `info my:module Message 1\n`, + "Logged expected message"); + t.is(stripAnsi(stderrWriteStub.getCall(1).args[0]), `info my:other:module Message 2\n`, + "Logged expected message"); +}); + +test.serial("Module filtering: Disable one module", (t) => { + const {stderrWriteStub} = t.context; + process.env.UI5_LOG_MODULES = "-my:module"; + t.context.consoleWriter = ConsoleWriter.init(); + + process.emit("ui5.log", {level: "info", message: "Message 1", moduleName: "my:module"}); + process.emit("ui5.log", {level: "info", message: "Message 2", moduleName: "other:module"}); + + t.is(stderrWriteStub.callCount, 1, "Logged one message"); + t.is(stripAnsi(stderrWriteStub.getCall(0).args[0]), `info other:module Message 2\n`, + "Logged expected message"); +}); + +test.serial("Module filtering: Disable one namespace", (t) => { + const {stderrWriteStub} = t.context; + process.env.UI5_LOG_MODULES = "-my:*"; + t.context.consoleWriter = ConsoleWriter.init(); + + process.emit("ui5.log", {level: "info", message: "Message 1", moduleName: "my:module"}); + process.emit("ui5.log", {level: "info", message: "Message 2", moduleName: "my:other:module"}); + process.emit("ui5.log", {level: "info", message: "Message 3", moduleName: "other:module"}); + + t.is(stderrWriteStub.callCount, 1, "Logged one message"); + t.is(stripAnsi(stderrWriteStub.getCall(0).args[0]), `info other:module Message 3\n`, + "Logged expected message"); +}); + +test.serial("Module filtering: Combination of settings", (t) => { + const {stderrWriteStub} = t.context; + process.env.UI5_LOG_MODULES = "my:*, -my:other:module, other:module"; + t.context.consoleWriter = ConsoleWriter.init(); + + process.emit("ui5.log", {level: "info", message: "Message 1", moduleName: "my:module"}); + process.emit("ui5.log", {level: "info", message: "Message 2", moduleName: "my:other:module"}); + process.emit("ui5.log", {level: "info", message: "Message 3", moduleName: "other:module"}); + process.emit("ui5.log", {level: "info", message: "Message 4", moduleName: "another:module"}); + + t.is(stderrWriteStub.callCount, 2, "Logged two messages"); + t.is(stripAnsi(stderrWriteStub.getCall(0).args[0]), `info my:module Message 1\n`, + "Logged expected message for my:module"); + t.is(stripAnsi(stderrWriteStub.getCall(1).args[0]), `info other:module Message 3\n`, + "Logged expected message for other:module"); +});