This is an automated email from the ASF dual-hosted git repository. borinquenkid pushed a commit to branch fix/stacktrace-filterer-test-log-capture in repository https://gitbox.apache.org/repos/asf/grails-core.git
commit 335a5b1f2dde2c6c4fbc6ec2caae322b2beb8c8d Author: Walter Duque de Estrada <[email protected]> AuthorDate: Wed Jul 29 15:47:46 2026 -0500 fix: capture STACK_LOG output via a Logback appender instead of System.err DefaultStackTraceFilterer.STACK_LOG routes through commons-logging, which resolves to a jcl-over-slf4j binding on this classpath -- so its output never touches System.err, regardless of test ordering or timing. Swapping System.err therefore never observes the emitted message, making GrailsUtilStackFiltererSpec and GrailsBootstrapRegistryInitializerSpec fail deterministically. Attach a ListAppender directly to the public STACK_LOG_NAME logger instead, which is unaffected by which commons-logging backend wins the classpath. Co-Authored-By: Claude Sonnet 5 <[email protected]> --- .../grails/util/GrailsUtilStackFiltererSpec.groovy | 47 +++++++++++++++------- .../GrailsBootstrapRegistryInitializerSpec.groovy | 43 ++++++++++++++------ 2 files changed, 64 insertions(+), 26 deletions(-) diff --git a/grails-core/src/test/groovy/grails/util/GrailsUtilStackFiltererSpec.groovy b/grails-core/src/test/groovy/grails/util/GrailsUtilStackFiltererSpec.groovy index 1b99ab7b6a..c7e650cfd1 100644 --- a/grails-core/src/test/groovy/grails/util/GrailsUtilStackFiltererSpec.groovy +++ b/grails-core/src/test/groovy/grails/util/GrailsUtilStackFiltererSpec.groovy @@ -18,8 +18,12 @@ */ package grails.util +import ch.qos.logback.classic.Logger +import ch.qos.logback.classic.spi.ILoggingEvent +import ch.qos.logback.core.read.ListAppender import org.grails.exceptions.reporting.DefaultStackTraceFilterer import org.grails.exceptions.reporting.StackTraceFilterer +import org.slf4j.LoggerFactory import spock.lang.Specification /** @@ -104,10 +108,8 @@ class GrailsUtilStackFiltererSpec extends Specification { } def 'installed DefaultStackTraceFilterer honours logFullStackTraceOnFilter=false'() { - given: 'captured System.err' - def originalErr = System.err - def baos = new ByteArrayOutputStream() - System.setErr(new PrintStream(baos, true)) + given: 'a recording appender on the dedicated STACK_LOG logger' + def appender = attachRecordingAppender() and: 'a filterer with the side-effect emission disabled' def quietFilterer = new DefaultStackTraceFilterer() @@ -118,18 +120,15 @@ class GrailsUtilStackFiltererSpec extends Specification { GrailsUtil.deepSanitize(exceptionWithApplicationFrame()) then: "no 'Full Stack Trace:' entry is emitted" - System.err.flush() - !baos.toString().contains(StackTraceFilterer.FULL_STACK_TRACE_MESSAGE) + appender.list.every { !it.formattedMessage.contains(StackTraceFilterer.FULL_STACK_TRACE_MESSAGE) } cleanup: - System.setErr(originalErr) + detachAppender(appender) } def 'installed DefaultStackTraceFilterer emits Full Stack Trace by default'() { - given: 'captured System.err' - def originalErr = System.err - def baos = new ByteArrayOutputStream() - System.setErr(new PrintStream(baos, true)) + given: 'a recording appender on the dedicated STACK_LOG logger' + def appender = attachRecordingAppender() and: 'a filterer with the default (enabled) side-effect emission' def loudFilterer = new DefaultStackTraceFilterer() @@ -139,11 +138,31 @@ class GrailsUtilStackFiltererSpec extends Specification { GrailsUtil.deepSanitize(exceptionWithApplicationFrame()) then: "a 'Full Stack Trace:' entry is emitted -- the positive control proving the negative case above is meaningful" - System.err.flush() - baos.toString().contains(StackTraceFilterer.FULL_STACK_TRACE_MESSAGE) + appender.list.any { it.formattedMessage.contains(StackTraceFilterer.FULL_STACK_TRACE_MESSAGE) } cleanup: - System.setErr(originalErr) + detachAppender(appender) + } + + /** + * {@link DefaultStackTraceFilterer#STACK_LOG} routes through commons-logging, which on this + * classpath resolves to a jcl-over-slf4j binding rather than a bare console writer -- so the + * emitted message never touches {@code System.err} for capture there. The logger name is a + * public constant precisely so tests and logging config can attach to it directly instead. + */ + private static ListAppender<ILoggingEvent> attachRecordingAppender() { + def logger = LoggerFactory.getLogger(DefaultStackTraceFilterer.STACK_LOG_NAME) as Logger + def appender = new ListAppender<ILoggingEvent>() + appender.context = logger.loggerContext + appender.start() + logger.addAppender(appender) + appender + } + + private static void detachAppender(ListAppender<ILoggingEvent> appender) { + def logger = LoggerFactory.getLogger(DefaultStackTraceFilterer.STACK_LOG_NAME) as Logger + logger.detachAppender(appender) + appender.stop() } private static RuntimeException exceptionWithApplicationFrame() { diff --git a/grails-core/src/test/groovy/org/apache/grails/core/GrailsBootstrapRegistryInitializerSpec.groovy b/grails-core/src/test/groovy/org/apache/grails/core/GrailsBootstrapRegistryInitializerSpec.groovy index 6197711401..a1e693887a 100644 --- a/grails-core/src/test/groovy/org/apache/grails/core/GrailsBootstrapRegistryInitializerSpec.groovy +++ b/grails-core/src/test/groovy/org/apache/grails/core/GrailsBootstrapRegistryInitializerSpec.groovy @@ -18,11 +18,15 @@ */ package org.apache.grails.core +import ch.qos.logback.classic.Logger +import ch.qos.logback.classic.spi.ILoggingEvent +import ch.qos.logback.core.read.ListAppender import grails.config.Settings import grails.util.GrailsUtil import org.grails.core.cfg.GroovyConfigPropertySourceLoader import org.grails.exceptions.reporting.DefaultStackTraceFilterer import org.grails.exceptions.reporting.StackTraceFilterer +import org.slf4j.LoggerFactory import org.springframework.boot.bootstrap.DefaultBootstrapContext import org.springframework.context.support.GenericApplicationContext import org.springframework.core.env.MapPropertySource @@ -225,39 +229,54 @@ class GrailsBootstrapRegistryInitializerSpec extends Specification { def context = contextWithProperties([ (Settings.SETTING_LOG_FULL_STACKTRACE_ON_FILTER): 'false' ]) - def originalErr = System.err - def baos = new ByteArrayOutputStream() - System.setErr(new PrintStream(baos, true)) + def appender = attachRecordingAppender() when: closeBootstrapContext(context) GrailsUtil.deepSanitize(exceptionWithApplicationFrame()) then: "no 'Full Stack Trace:' entry is emitted" - System.err.flush() - !baos.toString().contains(StackTraceFilterer.FULL_STACK_TRACE_MESSAGE) + appender.list.every { !it.formattedMessage.contains(StackTraceFilterer.FULL_STACK_TRACE_MESSAGE) } cleanup: - System.setErr(originalErr) + detachAppender(appender) } def 'defaults logFullStackTraceOnFilter to true on the promoted DefaultStackTraceFilterer'() { given: def context = contextWithProperties([:]) - def originalErr = System.err - def baos = new ByteArrayOutputStream() - System.setErr(new PrintStream(baos, true)) + def appender = attachRecordingAppender() when: closeBootstrapContext(context) GrailsUtil.deepSanitize(exceptionWithApplicationFrame()) then: 'the positive control proving the negative case above is meaningful' - System.err.flush() - baos.toString().contains(StackTraceFilterer.FULL_STACK_TRACE_MESSAGE) + appender.list.any { it.formattedMessage.contains(StackTraceFilterer.FULL_STACK_TRACE_MESSAGE) } cleanup: - System.setErr(originalErr) + detachAppender(appender) + } + + /** + * {@link DefaultStackTraceFilterer#STACK_LOG} routes through commons-logging, which on this + * classpath resolves to a jcl-over-slf4j binding rather than a bare console writer -- so the + * emitted message never touches {@code System.err} for capture there. The logger name is a + * public constant precisely so tests and logging config can attach to it directly instead. + */ + private static ListAppender<ILoggingEvent> attachRecordingAppender() { + def logger = LoggerFactory.getLogger(DefaultStackTraceFilterer.STACK_LOG_NAME) as Logger + def appender = new ListAppender<ILoggingEvent>() + appender.context = logger.loggerContext + appender.start() + logger.addAppender(appender) + appender + } + + private static void detachAppender(ListAppender<ILoggingEvent> appender) { + def logger = LoggerFactory.getLogger(DefaultStackTraceFilterer.STACK_LOG_NAME) as Logger + logger.detachAppender(appender) + appender.stop() } private static StackTraceFilterer promotedFilterer(GenericApplicationContext context) {
