This is an automated email from the ASF dual-hosted git repository.

paulk-asert pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git


The following commit(s) were added to refs/heads/master by this push:
     new 0a04376328 try to make JMX tests more resilient
0a04376328 is described below

commit 0a04376328dcec0a3442f3bb3b28723b52c1daf4
Author: Paul King <[email protected]>
AuthorDate: Wed May 13 23:20:24 2026 +1000

    try to make JMX tests more resilient
---
 .../builder/CgroupV2NpeMitigationExtension.groovy  | 79 ++++++++++++++++++
 .../jmx/builder/JmxBeanExportFactoryTest.groovy    |  2 +
 .../groovy/jmx/builder/JmxBeanFactoryTest.groovy   |  2 +
 .../groovy/jmx/builder/JmxBeansFactoryTest.groovy  |  2 +
 .../groovy/jmx/builder/JmxBuilderToolsTest.groovy  |  2 +
 .../builder/JmxClientConnectorFactoryTest.groovy   |  2 +
 .../jmx/builder/JmxListenerFactoryTest.groovy      | 96 ++++++----------------
 .../builder/JmxServerConnectorFactoryTest.groovy   |  2 +
 .../groovy/jmx/builder/JmxTimerFactoryTest.groovy  |  2 +
 9 files changed, 116 insertions(+), 73 deletions(-)

diff --git 
a/subprojects/groovy-jmx/src/test/groovy/groovy/jmx/builder/CgroupV2NpeMitigationExtension.groovy
 
b/subprojects/groovy-jmx/src/test/groovy/groovy/jmx/builder/CgroupV2NpeMitigationExtension.groovy
new file mode 100644
index 0000000000..61b9fe8b87
--- /dev/null
+++ 
b/subprojects/groovy-jmx/src/test/groovy/groovy/jmx/builder/CgroupV2NpeMitigationExtension.groovy
@@ -0,0 +1,79 @@
+/*
+ *  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 groovy.jmx.builder
+
+import org.junit.jupiter.api.Assumptions
+import org.junit.jupiter.api.extension.BeforeAllCallback
+import org.junit.jupiter.api.extension.ExtensionContext
+import org.junit.jupiter.api.extension.TestExecutionExceptionHandler
+
+import java.lang.management.ManagementFactory
+import java.util.logging.Level
+import java.util.logging.Logger
+
+/**
+ * Test-only JUnit 5 extension that absorbs the JDK cgroup-v2 first-use NPE
+ * (a null {@code anyController} in {@code CgroupV2Subsystem.getInstance})
+ * sporadically seen on some containerised CI hosts when JMX initialises the
+ * platform OS MXBean.
+ *
+ * <ul>
+ *   <li>{@link #beforeAll} primes the affected JDK code paths once per JVM,
+ *       so a one-shot race is absorbed before any test runs.</li>
+ *   <li>{@link #handleTestExecutionException} converts a deterministic
+ *       occurrence into a skipped assumption (via {@link Assumptions#abort})
+ *       instead of a test failure.</li>
+ * </ul>
+ *
+ * Applied via {@code @ExtendWith(CgroupV2NpeMitigationExtension)} on JMX
+ * test classes that touch the platform MBean server.
+ */
+class CgroupV2NpeMitigationExtension implements BeforeAllCallback, 
TestExecutionExceptionHandler {
+    private static final Logger LOGGER = 
Logger.getLogger(CgroupV2NpeMitigationExtension.name)
+    private static volatile boolean primed = false
+
+    @Override
+    void beforeAll(ExtensionContext context) {
+        if (primed) return
+        primed = true
+        try {
+            ManagementFactory.getOperatingSystemMXBean().toString()
+        } catch (Throwable t) {
+            LOGGER.log(Level.FINE, 'Primed OS MXBean init absorbed exception 
(likely JDK cgroup-v2)', t)
+        }
+        try {
+            new JmxBuilder().timer(name: 'prime:type=timer-prime', period: 
60000)
+        } catch (Throwable t) {
+            LOGGER.log(Level.FINE, 'Primed JmxBuilder.timer absorbed exception 
(likely JDK cgroup-v2)', t)
+        }
+    }
+
+    @Override
+    void handleTestExecutionException(ExtensionContext context, Throwable t) 
throws Throwable {
+        for (Throwable c = t; c != null; c = c.cause) {
+            String msg = c.message
+            if (c instanceof NullPointerException && msg != null &&
+                (msg.contains('CgroupInfo.getMountPoint') ||
+                        (msg.contains('anyController') && 
msg.contains('null')))) {
+                Assumptions.abort("Skipped: known JDK cgroup-v2 NPE in this CI 
environment: ${msg}")
+            }
+        }
+        throw t
+    }
+}
diff --git 
a/subprojects/groovy-jmx/src/test/groovy/groovy/jmx/builder/JmxBeanExportFactoryTest.groovy
 
b/subprojects/groovy-jmx/src/test/groovy/groovy/jmx/builder/JmxBeanExportFactoryTest.groovy
index 797c94ee71..23178cd43c 100644
--- 
a/subprojects/groovy-jmx/src/test/groovy/groovy/jmx/builder/JmxBeanExportFactoryTest.groovy
+++ 
b/subprojects/groovy-jmx/src/test/groovy/groovy/jmx/builder/JmxBeanExportFactoryTest.groovy
@@ -22,12 +22,14 @@ import groovy.jmx.GroovyMBean
 import org.junit.jupiter.api.Assumptions
 import org.junit.jupiter.api.BeforeEach
 import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
 
 import javax.management.MBeanServerConnection
 import javax.management.ObjectName
 
 import static groovy.test.GroovyAssert.shouldFail
 
+@ExtendWith(CgroupV2NpeMitigationExtension)
 final class JmxBeanExportFactoryTest {
 
     private JmxBuilder builder
diff --git 
a/subprojects/groovy-jmx/src/test/groovy/groovy/jmx/builder/JmxBeanFactoryTest.groovy
 
b/subprojects/groovy-jmx/src/test/groovy/groovy/jmx/builder/JmxBeanFactoryTest.groovy
index 0cb597b996..93689f891e 100644
--- 
a/subprojects/groovy-jmx/src/test/groovy/groovy/jmx/builder/JmxBeanFactoryTest.groovy
+++ 
b/subprojects/groovy-jmx/src/test/groovy/groovy/jmx/builder/JmxBeanFactoryTest.groovy
@@ -21,10 +21,12 @@ package groovy.jmx.builder
 import org.junit.jupiter.api.Assumptions
 import org.junit.jupiter.api.BeforeEach
 import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
 
 import javax.management.MBeanServerConnection
 import javax.management.ObjectName
 
+@ExtendWith(CgroupV2NpeMitigationExtension)
 final class JmxBeanFactoryTest {
 
     private JmxBuilder builder
diff --git 
a/subprojects/groovy-jmx/src/test/groovy/groovy/jmx/builder/JmxBeansFactoryTest.groovy
 
b/subprojects/groovy-jmx/src/test/groovy/groovy/jmx/builder/JmxBeansFactoryTest.groovy
index 52bf63a1c0..7ec36e76cf 100644
--- 
a/subprojects/groovy-jmx/src/test/groovy/groovy/jmx/builder/JmxBeansFactoryTest.groovy
+++ 
b/subprojects/groovy-jmx/src/test/groovy/groovy/jmx/builder/JmxBeansFactoryTest.groovy
@@ -21,7 +21,9 @@ package groovy.jmx.builder
 import org.junit.jupiter.api.Assumptions
 import org.junit.jupiter.api.BeforeEach
 import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
 
+@ExtendWith(CgroupV2NpeMitigationExtension)
 final class JmxBeansFactoryTest {
 
     private JmxBuilder builder
diff --git 
a/subprojects/groovy-jmx/src/test/groovy/groovy/jmx/builder/JmxBuilderToolsTest.groovy
 
b/subprojects/groovy-jmx/src/test/groovy/groovy/jmx/builder/JmxBuilderToolsTest.groovy
index 3e754f7c36..5ac1f64bfe 100644
--- 
a/subprojects/groovy-jmx/src/test/groovy/groovy/jmx/builder/JmxBuilderToolsTest.groovy
+++ 
b/subprojects/groovy-jmx/src/test/groovy/groovy/jmx/builder/JmxBuilderToolsTest.groovy
@@ -20,11 +20,13 @@ package groovy.jmx.builder
 
 import org.junit.jupiter.api.Assumptions
 import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
 
 import javax.management.ObjectName
 
 import static groovy.test.GroovyAssert.shouldFail
 
+@ExtendWith(CgroupV2NpeMitigationExtension)
 final class JmxBuilderToolsTest {
 
     @Test
diff --git 
a/subprojects/groovy-jmx/src/test/groovy/groovy/jmx/builder/JmxClientConnectorFactoryTest.groovy
 
b/subprojects/groovy-jmx/src/test/groovy/groovy/jmx/builder/JmxClientConnectorFactoryTest.groovy
index 28cd12b0c5..70e29eb6d4 100644
--- 
a/subprojects/groovy-jmx/src/test/groovy/groovy/jmx/builder/JmxClientConnectorFactoryTest.groovy
+++ 
b/subprojects/groovy-jmx/src/test/groovy/groovy/jmx/builder/JmxClientConnectorFactoryTest.groovy
@@ -23,6 +23,7 @@ import org.junit.jupiter.api.AfterEach
 import org.junit.jupiter.api.Assumptions
 import org.junit.jupiter.api.BeforeEach
 import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
 
 import javax.management.remote.rmi.RMIConnector
 import javax.management.remote.rmi.RMIConnectorServer
@@ -30,6 +31,7 @@ import javax.management.remote.rmi.RMIConnectorServer
 import static groovy.test.GroovyAssert.shouldFail
 
 @ForkedJvm
+@ExtendWith(CgroupV2NpeMitigationExtension)
 final class JmxClientConnectorFactoryTest {
 
     private JmxBuilder builder
diff --git 
a/subprojects/groovy-jmx/src/test/groovy/groovy/jmx/builder/JmxListenerFactoryTest.groovy
 
b/subprojects/groovy-jmx/src/test/groovy/groovy/jmx/builder/JmxListenerFactoryTest.groovy
index 208621853b..cf35a79fcc 100644
--- 
a/subprojects/groovy-jmx/src/test/groovy/groovy/jmx/builder/JmxListenerFactoryTest.groovy
+++ 
b/subprojects/groovy-jmx/src/test/groovy/groovy/jmx/builder/JmxListenerFactoryTest.groovy
@@ -18,40 +18,16 @@
  */
 package groovy.jmx.builder
 
-import org.junit.jupiter.api.Assumptions
 import org.junit.jupiter.api.BeforeEach
 import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
 
 import javax.management.ObjectName
-import java.lang.management.ManagementFactory
-import java.util.logging.Level
-import java.util.logging.Logger
 
 import static groovy.test.GroovyAssert.shouldFail
 
+@ExtendWith(CgroupV2NpeMitigationExtension)
 class JmxListenerFactoryTest {
-    private static final Logger LOGGER = 
Logger.getLogger(JmxListenerFactoryTest.name)
-
-    static {
-        // Prime the code paths that sporadically NPE on first use in 
containerized
-        // CI environments — JDK cgroup-v2 detection (CgroupV2Subsystem / 
CgroupInfo).
-        // JMX MBean registration walks the platform OS MXBean, which on Linux
-        // consults cgroup state. Where the trip is a one-shot first-use race,
-        // priming here absorbs it. Where the trip is deterministic (the JDK 
bug
-        // fires on every call in this env), the per-test guard below converts 
it
-        // into a skipped assumption instead of a failure.
-        try {
-            ManagementFactory.getOperatingSystemMXBean().toString()
-        } catch (Throwable t) {
-            LOGGER.log(Level.FINE, 'Primed OS MXBean init absorbed exception 
(likely JDK cgroup-v2)', t)
-        }
-        try {
-            new JmxBuilder().timer(name: 'prime:type=timer-prime', period: 
60000)
-        } catch (Throwable t) {
-            LOGGER.log(Level.FINE, 'Primed JmxBuilder.timer absorbed exception 
(likely JDK cgroup-v2)', t)
-        }
-    }
-
     def builder
 
     @BeforeEach
@@ -61,62 +37,36 @@ class JmxListenerFactoryTest {
 
     @Test
     void testRequiredAttributeFrom() {
-        try {
-            builder.timer(name: "test:type=timer")
-            def lstr = builder.listener(from: "test:type=timer")
-            assert lstr
-            assert lstr.type == "eventListener"
-            assert lstr.from instanceof ObjectName
-            assert lstr.from == new ObjectName("test:type=timer")
+        builder.timer(name: "test:type=timer")
+        def lstr = builder.listener(from: "test:type=timer")
+        assert lstr
+        assert lstr.type == "eventListener"
+        assert lstr.from instanceof ObjectName
+        assert lstr.from == new ObjectName("test:type=timer")
 
-            shouldFail {
-                lstr = builder.listener(event: "someEvent")
-                lstr = builder.listener(from: "test:type=nonExistingObject")
-            }
-        } catch (Throwable t) {
-            skipIfKnownCgroupNpe(t)
-            throw t
+        shouldFail {
+            lstr = builder.listener(event: "someEvent")
+            lstr = builder.listener(from: "test:type=nonExistingObject")
         }
     }
 
     @Test
     void testListenerEvent() {
-        try {
-            def eventCount = 0
-            builder.timer(name: "test:type=timer", period: 200).start()
+        def eventCount = 0
+        builder.timer(name: "test:type=timer", period: 200).start()
+        builder.listener(from: "test:type=timer", call: {event ->
+            eventCount = eventCount + 1
+        })
+        sleep 1300
+        assert eventCount > 1
+
+        shouldFail {
+            eventCount = 0
             builder.listener(from: "test:type=timer", call: {event ->
                 eventCount = eventCount + 1
             })
-            sleep 1300
-            assert eventCount > 1
-
-            shouldFail {
-                eventCount = 0
-                builder.listener(from: "test:type=timer", call: {event ->
-                    eventCount = eventCount + 1
-                })
-                sleep 700
-                assert eventCount == 0
-            }
-        } catch (Throwable t) {
-            skipIfKnownCgroupNpe(t)
-            throw t
-        }
-    }
-
-    /**
-     * If the given throwable (or any of its causes) is the known JDK cgroup-v2
-     * NPE — {@code CgroupInfo.getMountPoint()} on a null controller — abort 
the
-     * test with an assumption (reported as skipped, not failed). Any other
-     * failure propagates unchanged.
-     */
-    private static void skipIfKnownCgroupNpe(Throwable t) {
-        for (Throwable c = t; c != null; c = c.cause) {
-            String msg = c.message
-            if (c instanceof NullPointerException && msg != null &&
-                (msg.contains('CgroupInfo.getMountPoint') || 
msg.contains('anyController') && msg.contains('null'))) {
-                Assumptions.abort("Skipped: known JDK cgroup-v2 NPE in this CI 
environment: ${msg}")
-            }
+            sleep 700
+            assert eventCount == 0
         }
     }
 }
diff --git 
a/subprojects/groovy-jmx/src/test/groovy/groovy/jmx/builder/JmxServerConnectorFactoryTest.groovy
 
b/subprojects/groovy-jmx/src/test/groovy/groovy/jmx/builder/JmxServerConnectorFactoryTest.groovy
index 139a65f46c..dd7cbe588a 100644
--- 
a/subprojects/groovy-jmx/src/test/groovy/groovy/jmx/builder/JmxServerConnectorFactoryTest.groovy
+++ 
b/subprojects/groovy-jmx/src/test/groovy/groovy/jmx/builder/JmxServerConnectorFactoryTest.groovy
@@ -21,12 +21,14 @@ package groovy.jmx.builder
 import org.junit.jupiter.api.AfterEach
 import org.junit.jupiter.api.BeforeEach
 import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
 
 import javax.management.remote.JMXConnector
 import javax.management.remote.JMXConnectorFactory
 import javax.management.remote.JMXServiceURL
 import javax.management.remote.rmi.RMIConnectorServer
 
+@ExtendWith(CgroupV2NpeMitigationExtension)
 class JmxServerConnectorFactoryTest {
     def builder
     int defaultPort = 10995
diff --git 
a/subprojects/groovy-jmx/src/test/groovy/groovy/jmx/builder/JmxTimerFactoryTest.groovy
 
b/subprojects/groovy-jmx/src/test/groovy/groovy/jmx/builder/JmxTimerFactoryTest.groovy
index eb1c238cdf..782ec2ecde 100644
--- 
a/subprojects/groovy-jmx/src/test/groovy/groovy/jmx/builder/JmxTimerFactoryTest.groovy
+++ 
b/subprojects/groovy-jmx/src/test/groovy/groovy/jmx/builder/JmxTimerFactoryTest.groovy
@@ -22,12 +22,14 @@ import groovy.jmx.GroovyMBean
 import org.junit.jupiter.api.Assumptions
 import org.junit.jupiter.api.BeforeEach
 import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
 
 import javax.management.MBeanServerConnection
 import javax.management.ObjectName
 
 import static groovy.test.GroovyAssert.shouldFail
 
+@ExtendWith(CgroupV2NpeMitigationExtension)
 final class JmxTimerFactoryTest {
 
     private JmxBuilder builder

Reply via email to