From 65742667a6db9ed1d6d079064ee8881749178013 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Fri, 7 Aug 2026 11:32:02 +0200 Subject: [PATCH 1/4] Backport Log API enhancements and mojo MDC to 4.0.x MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backport four Log-related improvements from master to the 4.0.x branch for inclusion in rc-7: 1. Log.trace() — new trace level (maps to SLF4J TRACE / JUL FINEST) to separate Maven core internals from user-facing debug messages. Currently -X floods debug output with resolver/interpolation details that drown user-relevant diagnostics. 2. Log.child(name) — creates a sub-logger with an independently filterable name (e.g. "CompilerMojo.diagnostics"), letting plugin sub-components log under their own namespace. 3. Logger name alignment — Maven 4 Log now uses the mojo implementation class name (e.g. "org.apache.maven.plugins.compiler.CompilerMojo") instead of the goal name ("compiler:compile"). This matches what Maven 3 mojos already use and enables standard SLF4J hierarchical level configuration. 4. Mojo MDC propagation — sets "maven.mojo.id" (prefix:goal@executionId) in the SLF4J MDC during mojo execution. All log messages — including those arriving through the JUL-to-SLF4J bridge — now carry mojo context, available to any SLF4J appender via %X{maven.mojo.id}. Also fixes a pre-existing bug in DefaultLog where warn(Supplier, Throwable) incorrectly delegated to logger.info() instead of logger.warn(). Co-Authored-By: Claude Opus 4.6 --- .../java/org/apache/maven/api/plugin/Log.java | 80 +++++++++++++++++++ .../maven/internal/impl/DefaultLog.java | 47 ++++++++++- .../logging/LoggingExecutionListener.java | 11 +++ .../logging/ProjectBuildLogAppender.java | 27 +++++++ .../plugin/DefaultBuildPluginManager.java | 2 +- .../internal/DefaultMavenPluginManager.java | 2 +- 6 files changed, 166 insertions(+), 3 deletions(-) diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/plugin/Log.java b/api/maven-api-core/src/main/java/org/apache/maven/api/plugin/Log.java index f2968295e456..a0cd78fd39d4 100644 --- a/api/maven-api-core/src/main/java/org/apache/maven/api/plugin/Log.java +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/plugin/Log.java @@ -36,6 +36,58 @@ @Experimental @Provider public interface Log { + /** + * {@return true if the trace error level is enabled} + */ + boolean isTraceEnabled(); + + /** + * Sends a message to the user in the trace error level. + *

+ * Trace is the most verbose level, intended for Maven core internals + * such as resolver negotiation, model interpolation, and lifecycle + * ordering details. Use {@link #debug(CharSequence)} instead for + * messages that help users investigate their build + * (e.g. why a module was recompiled). + * + * @param content the message to log + */ + void trace(CharSequence content); + + /** + * Sends a message (and accompanying exception) to the user at the trace error level. + * The error's stacktrace will be output when this error level is enabled. + * + * @param content the message to log + * @param error the error that caused this log + */ + void trace(CharSequence content, Throwable error); + + /** + * Sends an exception to the user in the trace error level. + * The stack trace for this exception will be output when this error level is enabled. + * + * @param error the error that caused this log + */ + void trace(Throwable error); + + /** + * Sends a lazily-computed message in the trace error level. + * The supplier is only evaluated if trace is enabled. + * + * @param content the message supplier + */ + void trace(Supplier content); + + /** + * Sends a lazily-computed message (and accompanying exception) in the trace error level. + * The supplier is only evaluated if trace is enabled. + * + * @param content the message supplier + * @param error the error that caused this log + */ + void trace(Supplier content, Throwable error); + /** * {@return true if the debug error level is enabled} */ @@ -43,6 +95,11 @@ public interface Log { /** * Sends a message to the user in the debug error level. + *

+ * Debug is intended for messages that help users investigate + * their build — for example, why a module was recompiled or what + * classpath was resolved. For Maven core internals, use + * {@link #trace(CharSequence)} instead. * * @param content the message to log */ @@ -167,4 +224,27 @@ public interface Log { void error(Supplier content); void error(Supplier content, Throwable error); + + /** + * Returns a child logger whose name is derived from this logger's name + * by appending a dot and the given suffix. + * + *

For example, if a plugin's logger is named + * {@code "org.apache.maven.plugins.compiler.CompilerMojo"}, + * then {@code child("diagnostics")} returns a logger named + * {@code "org.apache.maven.plugins.compiler.CompilerMojo.diagnostics"}. + * This lets sub-components log under an independently filterable name + * without requiring a separate injection point.

+ * + *

The default implementation returns {@code this}, so existing + * {@code Log} implementations continue to work without changes. + * Implementations that wrap a hierarchical logging backend (such as + * SLF4J) should override this to create a real child logger.

+ * + * @param name the suffix to append (must not be {@code null} or blank) + * @return a child logger — never {@code null} + */ + default Log child(String name) { + return this; + } } diff --git a/impl/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultLog.java b/impl/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultLog.java index 1a11fe46fdb5..a70c2adb290a 100644 --- a/impl/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultLog.java +++ b/impl/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultLog.java @@ -22,6 +22,7 @@ import org.apache.maven.api.plugin.Log; import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import static java.util.Objects.requireNonNull; @@ -32,6 +33,44 @@ public DefaultLog(Logger logger) { this.logger = requireNonNull(logger); } + @Override + public boolean isTraceEnabled() { + return logger.isTraceEnabled(); + } + + @Override + public void trace(CharSequence content) { + if (isTraceEnabled()) { + logger.trace(toString(content)); + } + } + + @Override + public void trace(CharSequence content, Throwable error) { + if (isTraceEnabled()) { + logger.trace(toString(content), error); + } + } + + @Override + public void trace(Throwable error) { + logger.trace("", error); + } + + @Override + public void trace(Supplier content) { + if (isTraceEnabled()) { + logger.trace(content.get()); + } + } + + @Override + public void trace(Supplier content, Throwable error) { + if (isTraceEnabled()) { + logger.trace(content.get(), error); + } + } + @Override public void debug(CharSequence content) { if (isDebugEnabled()) { @@ -127,7 +166,7 @@ public void warn(Supplier content) { @Override public void warn(Supplier content, Throwable error) { if (isWarnEnabled()) { - logger.info(content.get(), error); + logger.warn(content.get(), error); } } @@ -184,6 +223,12 @@ public boolean isErrorEnabled() { return logger.isErrorEnabled(); } + @Override + public Log child(String name) { + requireNonNull(name, "name"); + return new DefaultLog(LoggerFactory.getLogger(logger.getName() + "." + name)); + } + private String toString(CharSequence content) { return content != null ? content.toString() : ""; } diff --git a/impl/maven-core/src/main/java/org/apache/maven/logging/LoggingExecutionListener.java b/impl/maven-core/src/main/java/org/apache/maven/logging/LoggingExecutionListener.java index 040a481455e2..6685351b362a 100644 --- a/impl/maven-core/src/main/java/org/apache/maven/logging/LoggingExecutionListener.java +++ b/impl/maven-core/src/main/java/org/apache/maven/logging/LoggingExecutionListener.java @@ -121,6 +121,7 @@ public void projectSkipped(ExecutionEvent event) { @Override public void mojoStarted(ExecutionEvent event) { setMdc(event); + setMojoMdc(event); buildEventListener.mojoStarted(event); delegate.mojoStarted(event); } @@ -128,12 +129,14 @@ public void mojoStarted(ExecutionEvent event) { @Override public void mojoSucceeded(ExecutionEvent event) { setMdc(event); + ProjectBuildLogAppender.setMojoId(null); delegate.mojoSucceeded(event); } @Override public void mojoFailed(ExecutionEvent event) { setMdc(event); + ProjectBuildLogAppender.setMojoId(null); delegate.mojoFailed(event); } @@ -187,4 +190,12 @@ private void setMdc(ExecutionEvent event) { ProjectBuildLogAppender.setProjectId(event.getProject().getArtifactId()); } } + + private void setMojoMdc(ExecutionEvent event) { + if (event.getMojoExecution() != null) { + String mojoId = event.getMojoExecution().getMojoDescriptor().getFullGoalName() + "@" + + event.getMojoExecution().getExecutionId(); + ProjectBuildLogAppender.setMojoId(mojoId); + } + } } diff --git a/impl/maven-core/src/main/java/org/apache/maven/logging/ProjectBuildLogAppender.java b/impl/maven-core/src/main/java/org/apache/maven/logging/ProjectBuildLogAppender.java index 8465df0cf060..947d09474f21 100644 --- a/impl/maven-core/src/main/java/org/apache/maven/logging/ProjectBuildLogAppender.java +++ b/impl/maven-core/src/main/java/org/apache/maven/logging/ProjectBuildLogAppender.java @@ -27,7 +27,9 @@ public class ProjectBuildLogAppender implements AutoCloseable { private static final String KEY_PROJECT_ID = "maven.project.id"; + private static final String KEY_MOJO_ID = "maven.mojo.id"; private static final ThreadLocal PROJECT_ID = new InheritableThreadLocal<>(); + private static final ThreadLocal MOJO_ID = new InheritableThreadLocal<>(); private static final ThreadLocal FORKING_PROJECT_ID = new InheritableThreadLocal<>(); public static String getProjectId() { @@ -52,6 +54,31 @@ public static void setProjectId(String projectId) { } } + public static String getMojoId() { + return MOJO_ID.get(); + } + + /** + * Sets or clears the mojo execution identifier in both the thread-local + * and the SLF4J MDC. The value is available to any SLF4J appender via + * the MDC key {@code maven.mojo.id} and to JUL-bridged messages through + * the same MDC path. + *

+ * Format: {@code "prefix:goal@executionId"} + * (e.g. {@code "compiler:compile@default-compile"}). + * + * @param mojoId the mojo identifier, or {@code null} to clear + */ + public static void setMojoId(String mojoId) { + if (mojoId != null) { + MOJO_ID.set(mojoId); + MDC.put(KEY_MOJO_ID, mojoId); + } else { + MOJO_ID.remove(); + MDC.remove(KEY_MOJO_ID); + } + } + public static void setForkingProjectId(String forkingProjectId) { if (forkingProjectId != null) { FORKING_PROJECT_ID.set(forkingProjectId); diff --git a/impl/maven-core/src/main/java/org/apache/maven/plugin/DefaultBuildPluginManager.java b/impl/maven-core/src/main/java/org/apache/maven/plugin/DefaultBuildPluginManager.java index e395d1ed000b..476f2aadcdd3 100644 --- a/impl/maven-core/src/main/java/org/apache/maven/plugin/DefaultBuildPluginManager.java +++ b/impl/maven-core/src/main/java/org/apache/maven/plugin/DefaultBuildPluginManager.java @@ -125,7 +125,7 @@ public void executeMojo(MavenSession session, MojoExecution mojoExecution) scope.seed( org.apache.maven.api.plugin.Log.class, new DefaultLog(LoggerFactory.getLogger( - mojoExecution.getMojoDescriptor().getFullGoalName()))); + mojoExecution.getMojoDescriptor().getImplementation()))); InternalMavenSession sessionV4 = InternalMavenSession.from(session.getSession()); scope.seed(Project.class, sessionV4.getProject(project)); scope.seed(org.apache.maven.api.MojoExecution.class, new DefaultMojoExecution(sessionV4, mojoExecution)); diff --git a/impl/maven-core/src/main/java/org/apache/maven/plugin/internal/DefaultMavenPluginManager.java b/impl/maven-core/src/main/java/org/apache/maven/plugin/internal/DefaultMavenPluginManager.java index 15f3d8df7b64..62a39f72fb8c 100644 --- a/impl/maven-core/src/main/java/org/apache/maven/plugin/internal/DefaultMavenPluginManager.java +++ b/impl/maven-core/src/main/java/org/apache/maven/plugin/internal/DefaultMavenPluginManager.java @@ -555,7 +555,7 @@ private T loadV4Mojo( org.apache.maven.api.MojoExecution execution = new DefaultMojoExecution(sessionV4, mojoExecution); org.apache.maven.api.plugin.Log log = new DefaultLog( - LoggerFactory.getLogger(mojoExecution.getMojoDescriptor().getFullGoalName())); + LoggerFactory.getLogger(mojoExecution.getMojoDescriptor().getImplementation())); try { Injector injector = Injector.create(); injector.discover(pluginRealm); From 8ea621316612e5e037e555738285d8f83b7c1e59 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Sat, 8 Aug 2026 14:42:14 +0200 Subject: [PATCH 2/4] Add isXxxEnabled() guards to Throwable-only log overloads Align with master by wrapping the five xxx(Throwable) overloads in level-enabled checks, avoiding unnecessary method calls and empty string construction when the level is disabled. Co-Authored-By: Claude Opus 4.6 --- .../maven/internal/impl/DefaultLog.java | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/impl/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultLog.java b/impl/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultLog.java index a70c2adb290a..b1cf40cc4059 100644 --- a/impl/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultLog.java +++ b/impl/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultLog.java @@ -54,7 +54,9 @@ public void trace(CharSequence content, Throwable error) { @Override public void trace(Throwable error) { - logger.trace("", error); + if (isTraceEnabled()) { + logger.trace("", error); + } } @Override @@ -87,7 +89,9 @@ public void debug(CharSequence content, Throwable error) { @Override public void debug(Throwable error) { - logger.debug("", error); + if (isDebugEnabled()) { + logger.debug("", error); + } } @Override @@ -120,7 +124,9 @@ public void info(CharSequence content, Throwable error) { @Override public void info(Throwable error) { - logger.info("", error); + if (isInfoEnabled()) { + logger.info("", error); + } } @Override @@ -153,7 +159,9 @@ public void warn(CharSequence content, Throwable error) { @Override public void warn(Throwable error) { - logger.warn("", error); + if (isWarnEnabled()) { + logger.warn("", error); + } } @Override @@ -186,7 +194,9 @@ public void error(CharSequence content, Throwable error) { @Override public void error(Throwable error) { - logger.error("", error); + if (isErrorEnabled()) { + logger.error("", error); + } } @Override From e3c872f56d23c64707adc32d6570941d61564bfa Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Sat, 29 Aug 2026 21:47:28 +0200 Subject: [PATCH 3/4] Address review: default trace methods and fork-aware mojoId MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apply review fixes from #12694 to align the backport: - Log.java: make all 6 trace methods default (no-ops) to prevent AbstractMethodError for existing third-party Log implementors. isTraceEnabled() returns false by default. - ProjectBuildLogAppender: add FORKING_MOJO_ID ThreadLocal mirroring the existing FORKING_PROJECT_ID pattern. When setMojoId(null) is called, the forking mojo's ID is restored instead of clearing. - LoggingExecutionListener: save current mojoId in forkStarted(), clear forking mojoId in forkSucceeded/forkFailed. Fix cleanup ordering in mojoSucceeded/mojoFailed — delegate runs first, then MDC is cleared. Co-Authored-By: Claude Opus 4.6 --- .../java/org/apache/maven/api/plugin/Log.java | 27 ++++++++++++---- .../logging/LoggingExecutionListener.java | 8 +++-- .../logging/ProjectBuildLogAppender.java | 32 ++++++++++++++++--- 3 files changed, 54 insertions(+), 13 deletions(-) diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/plugin/Log.java b/api/maven-api-core/src/main/java/org/apache/maven/api/plugin/Log.java index a0cd78fd39d4..a50d86c48b3a 100644 --- a/api/maven-api-core/src/main/java/org/apache/maven/api/plugin/Log.java +++ b/api/maven-api-core/src/main/java/org/apache/maven/api/plugin/Log.java @@ -38,8 +38,13 @@ public interface Log { /** * {@return true if the trace error level is enabled} + *

+ * The default implementation returns {@code false} for backward + * compatibility with existing {@code Log} implementations. */ - boolean isTraceEnabled(); + default boolean isTraceEnabled() { + return false; + } /** * Sends a message to the user in the trace error level. @@ -49,44 +54,54 @@ public interface Log { * ordering details. Use {@link #debug(CharSequence)} instead for * messages that help users investigate their build * (e.g. why a module was recompiled). + *

+ * The default implementation is a no-op for backward compatibility. * * @param content the message to log */ - void trace(CharSequence content); + default void trace(CharSequence content) {} /** * Sends a message (and accompanying exception) to the user at the trace error level. * The error's stacktrace will be output when this error level is enabled. + *

+ * The default implementation is a no-op for backward compatibility. * * @param content the message to log * @param error the error that caused this log */ - void trace(CharSequence content, Throwable error); + default void trace(CharSequence content, Throwable error) {} /** * Sends an exception to the user in the trace error level. * The stack trace for this exception will be output when this error level is enabled. + *

+ * The default implementation is a no-op for backward compatibility. * * @param error the error that caused this log */ - void trace(Throwable error); + default void trace(Throwable error) {} /** * Sends a lazily-computed message in the trace error level. * The supplier is only evaluated if trace is enabled. + *

+ * The default implementation is a no-op for backward compatibility. * * @param content the message supplier */ - void trace(Supplier content); + default void trace(Supplier content) {} /** * Sends a lazily-computed message (and accompanying exception) in the trace error level. * The supplier is only evaluated if trace is enabled. + *

+ * The default implementation is a no-op for backward compatibility. * * @param content the message supplier * @param error the error that caused this log */ - void trace(Supplier content, Throwable error); + default void trace(Supplier content, Throwable error) {} /** * {@return true if the debug error level is enabled} diff --git a/impl/maven-core/src/main/java/org/apache/maven/logging/LoggingExecutionListener.java b/impl/maven-core/src/main/java/org/apache/maven/logging/LoggingExecutionListener.java index 6685351b362a..7ca89b12b8bb 100644 --- a/impl/maven-core/src/main/java/org/apache/maven/logging/LoggingExecutionListener.java +++ b/impl/maven-core/src/main/java/org/apache/maven/logging/LoggingExecutionListener.java @@ -129,15 +129,15 @@ public void mojoStarted(ExecutionEvent event) { @Override public void mojoSucceeded(ExecutionEvent event) { setMdc(event); - ProjectBuildLogAppender.setMojoId(null); delegate.mojoSucceeded(event); + ProjectBuildLogAppender.setMojoId(null); } @Override public void mojoFailed(ExecutionEvent event) { setMdc(event); - ProjectBuildLogAppender.setMojoId(null); delegate.mojoFailed(event); + ProjectBuildLogAppender.setMojoId(null); } @Override @@ -151,18 +151,22 @@ public void forkStarted(ExecutionEvent event) { setMdc(event); delegate.forkStarted(event); ProjectBuildLogAppender.setForkingProjectId(event.getProject().getArtifactId()); + // Save the forking mojo's ID so it can be restored when the fork completes + ProjectBuildLogAppender.setForkingMojoId(ProjectBuildLogAppender.getMojoId()); } @Override public void forkSucceeded(ExecutionEvent event) { delegate.forkSucceeded(event); ProjectBuildLogAppender.setForkingProjectId(null); + ProjectBuildLogAppender.setForkingMojoId(null); } @Override public void forkFailed(ExecutionEvent event) { delegate.forkFailed(event); ProjectBuildLogAppender.setForkingProjectId(null); + ProjectBuildLogAppender.setForkingMojoId(null); } @Override diff --git a/impl/maven-core/src/main/java/org/apache/maven/logging/ProjectBuildLogAppender.java b/impl/maven-core/src/main/java/org/apache/maven/logging/ProjectBuildLogAppender.java index 947d09474f21..dc82a2f89848 100644 --- a/impl/maven-core/src/main/java/org/apache/maven/logging/ProjectBuildLogAppender.java +++ b/impl/maven-core/src/main/java/org/apache/maven/logging/ProjectBuildLogAppender.java @@ -31,6 +31,7 @@ public class ProjectBuildLogAppender implements AutoCloseable { private static final ThreadLocal PROJECT_ID = new InheritableThreadLocal<>(); private static final ThreadLocal MOJO_ID = new InheritableThreadLocal<>(); private static final ThreadLocal FORKING_PROJECT_ID = new InheritableThreadLocal<>(); + private static final ThreadLocal FORKING_MOJO_ID = new InheritableThreadLocal<>(); public static String getProjectId() { return PROJECT_ID.get(); @@ -60,9 +61,8 @@ public static String getMojoId() { /** * Sets or clears the mojo execution identifier in both the thread-local - * and the SLF4J MDC. The value is available to any SLF4J appender via - * the MDC key {@code maven.mojo.id} and to JUL-bridged messages through - * the same MDC path. + * and the SLF4J MDC. When clearing ({@code null}), if a forking mojo ID + * was saved, it is restored — mirroring the fork-aware project ID pattern. *

* Format: {@code "prefix:goal@executionId"} * (e.g. {@code "compiler:compile@default-compile"}). @@ -74,8 +74,15 @@ public static void setMojoId(String mojoId) { MOJO_ID.set(mojoId); MDC.put(KEY_MOJO_ID, mojoId); } else { - MOJO_ID.remove(); - MDC.remove(KEY_MOJO_ID); + // Restore the forking mojo's ID if one was saved + String forkingMojoId = FORKING_MOJO_ID.get(); + if (forkingMojoId != null) { + MOJO_ID.set(forkingMojoId); + MDC.put(KEY_MOJO_ID, forkingMojoId); + } else { + MOJO_ID.remove(); + MDC.remove(KEY_MOJO_ID); + } } } @@ -87,6 +94,21 @@ public static void setForkingProjectId(String forkingProjectId) { } } + /** + * Saves or clears the mojo ID of the forking mojo, so it can be + * restored when the fork completes. Mirrors the {@link #setForkingProjectId} + * pattern for project IDs. + * + * @param forkingMojoId the forking mojo identifier, or {@code null} to clear + */ + public static void setForkingMojoId(String forkingMojoId) { + if (forkingMojoId != null) { + FORKING_MOJO_ID.set(forkingMojoId); + } else { + FORKING_MOJO_ID.remove(); + } + } + public static void updateMdc() { String id = getProjectId(); if (id != null) { From 56f4be346facbd0649ec1e9ac1ec05afd76aaa71 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Sat, 29 Aug 2026 23:56:15 +0200 Subject: [PATCH 4/4] Address review: add DefaultLogTest and clear MDC on mojoSkipped - Add DefaultLogTest with 5 tests: warn/supplier regression, trace delegation, trace no-op guard, child() sub-logger, and default trace methods (AbstractMethodError prevention). - Clear mojo MDC in mojoSkipped() to prevent stale mojo context from leaking into subsequent log messages. Co-Authored-By: Claude Opus 4.6 --- .../logging/LoggingExecutionListener.java | 1 + .../maven/internal/impl/DefaultLogTest.java | 196 ++++++++++++++++++ 2 files changed, 197 insertions(+) create mode 100644 impl/maven-core/src/test/java/org/apache/maven/internal/impl/DefaultLogTest.java diff --git a/impl/maven-core/src/main/java/org/apache/maven/logging/LoggingExecutionListener.java b/impl/maven-core/src/main/java/org/apache/maven/logging/LoggingExecutionListener.java index 7ca89b12b8bb..700834eb80df 100644 --- a/impl/maven-core/src/main/java/org/apache/maven/logging/LoggingExecutionListener.java +++ b/impl/maven-core/src/main/java/org/apache/maven/logging/LoggingExecutionListener.java @@ -144,6 +144,7 @@ public void mojoFailed(ExecutionEvent event) { public void mojoSkipped(ExecutionEvent event) { setMdc(event); delegate.mojoSkipped(event); + ProjectBuildLogAppender.setMojoId(null); } @Override diff --git a/impl/maven-core/src/test/java/org/apache/maven/internal/impl/DefaultLogTest.java b/impl/maven-core/src/test/java/org/apache/maven/internal/impl/DefaultLogTest.java new file mode 100644 index 000000000000..eb290734fb98 --- /dev/null +++ b/impl/maven-core/src/test/java/org/apache/maven/internal/impl/DefaultLogTest.java @@ -0,0 +1,196 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.internal.impl; + +import org.apache.maven.api.plugin.Log; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +/** + * Tests for {@link DefaultLog}. + */ +class DefaultLogTest { + + /** + * Regression test: {@code warn(Supplier, Throwable)} was incorrectly + * calling {@code logger.info()} instead of {@code logger.warn()}. + */ + @Test + void warnWithSupplierAndThrowableDelegatesToWarn() { + Logger mockLogger = mock(Logger.class); + when(mockLogger.isWarnEnabled()).thenReturn(true); + + DefaultLog log = new DefaultLog(mockLogger); + RuntimeException ex = new RuntimeException("test"); + log.warn(() -> "warning message", ex); + + verify(mockLogger).warn("warning message", ex); + } + + /** + * Verify trace methods delegate to the SLF4J logger correctly. + */ + @Test + void traceMethodsDelegateToSlf4jTrace() { + Logger mockLogger = mock(Logger.class); + when(mockLogger.isTraceEnabled()).thenReturn(true); + + DefaultLog log = new DefaultLog(mockLogger); + log.trace("trace message"); + + verify(mockLogger).trace("trace message"); + } + + /** + * Verify that trace methods are no-ops when trace is disabled. + */ + @Test + void traceIsNoOpWhenDisabled() { + Logger mockLogger = mock(Logger.class); + when(mockLogger.isTraceEnabled()).thenReturn(false); + + DefaultLog log = new DefaultLog(mockLogger); + log.trace("should not be logged"); + + verify(mockLogger).isTraceEnabled(); + verifyNoMoreInteractions(mockLogger); + } + + /** + * Verify that {@code child()} creates a new logger with the + * expected hierarchical name. + */ + @Test + void childCreatesSubLogger() { + Logger mockLogger = mock(Logger.class); + when(mockLogger.getName()).thenReturn("org.apache.maven.plugins.compiler.CompilerMojo"); + + DefaultLog parent = new DefaultLog(mockLogger); + Log child = parent.child("diagnostics"); + + assertNotSame(parent, child); + assertTrue(child instanceof DefaultLog, "child should be a DefaultLog"); + } + + /** + * Verify that the default {@code Log.isTraceEnabled()} returns false, + * preventing {@code AbstractMethodError} for third-party implementors. + */ + @Test + void defaultTraceIsDisabled() { + // Use a minimal Log implementation that relies on defaults + Log minimal = new Log() { + @Override + public boolean isDebugEnabled() { + return false; + } + + @Override + public void debug(CharSequence c) {} + + @Override + public void debug(CharSequence c, Throwable e) {} + + @Override + public void debug(Throwable e) {} + + @Override + public void debug(java.util.function.Supplier c) {} + + @Override + public void debug(java.util.function.Supplier c, Throwable e) {} + + @Override + public boolean isInfoEnabled() { + return false; + } + + @Override + public void info(CharSequence c) {} + + @Override + public void info(CharSequence c, Throwable e) {} + + @Override + public void info(Throwable e) {} + + @Override + public void info(java.util.function.Supplier c) {} + + @Override + public void info(java.util.function.Supplier c, Throwable e) {} + + @Override + public boolean isWarnEnabled() { + return false; + } + + @Override + public void warn(CharSequence c) {} + + @Override + public void warn(CharSequence c, Throwable e) {} + + @Override + public void warn(Throwable e) {} + + @Override + public void warn(java.util.function.Supplier c) {} + + @Override + public void warn(java.util.function.Supplier c, Throwable e) {} + + @Override + public boolean isErrorEnabled() { + return false; + } + + @Override + public void error(CharSequence c) {} + + @Override + public void error(CharSequence c, Throwable e) {} + + @Override + public void error(Throwable e) {} + + @Override + public void error(java.util.function.Supplier c) {} + + @Override + public void error(java.util.function.Supplier c, Throwable e) {} + }; + + // These should NOT throw AbstractMethodError — they use defaults + assertEquals(false, minimal.isTraceEnabled()); + minimal.trace("should be a no-op"); + minimal.trace("no-op", new RuntimeException()); + minimal.trace(new RuntimeException()); + minimal.trace(() -> "no-op"); + minimal.trace(() -> "no-op", new RuntimeException()); + } +}