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) {

Reply via email to