diff --git a/blackbox-global-scope-teardown/pom.xml b/blackbox-global-scope-teardown/pom.xml new file mode 100644 index 000000000..55b2dbd8b --- /dev/null +++ b/blackbox-global-scope-teardown/pom.xml @@ -0,0 +1,90 @@ + + + + avaje-inject-parent + io.avaje + 12.7-RC2 + + 4.0.0 + + blackbox-global-scope-teardown + + + Reproduces global test scope corruption with real JVM-global state: ServerStatus is a + singleton that registers itself on the platform MBeanServer in PostConstruct and + unregisters in PreDestroy, the standard JMX pattern. T1 (plain InjectTest) passes. + T2 adds one Mockito mock of an unrelated interface and never references ServerStatus; + wiring its test scope rebuilds the entire application beside the live global scope, + constructs ServerStatus a second time, and fails with InstanceAlreadyExistsException. + Closing that failed scope runs the duplicate's PreDestroy, which unregisters (by + ObjectName) the MBean the live global scope owns, so T3 (identical to T1) then fails + with InstanceNotFoundException. T1 and T3 pass in isolation; T2 fails even alone. + + + + + io.avaje + avaje-inject + ${project.version} + + + jakarta.annotation + jakarta.annotation-api + 3.0.0 + + + io.avaje + avaje-inject-test + ${project.version} + test + + + io.avaje + junit + 1.8 + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + io.avaje + avaje-inject-generator + ${project.version} + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + alphabetical + + + + + io.avaje + avaje-inject-maven-plugin + ${project.version} + + + + process-sources + + provides + + + + + + + diff --git a/blackbox-global-scope-teardown/src/main/java/org/example/globalscope/DefaultGreeter.java b/blackbox-global-scope-teardown/src/main/java/org/example/globalscope/DefaultGreeter.java new file mode 100644 index 000000000..6b49bac50 --- /dev/null +++ b/blackbox-global-scope-teardown/src/main/java/org/example/globalscope/DefaultGreeter.java @@ -0,0 +1,11 @@ +package org.example.globalscope; + +import jakarta.inject.Singleton; + +@Singleton +public class DefaultGreeter implements Greeter { + @Override + public String greet() { + return "hello"; + } +} diff --git a/blackbox-global-scope-teardown/src/main/java/org/example/globalscope/Greeter.java b/blackbox-global-scope-teardown/src/main/java/org/example/globalscope/Greeter.java new file mode 100644 index 000000000..822edcc88 --- /dev/null +++ b/blackbox-global-scope-teardown/src/main/java/org/example/globalscope/Greeter.java @@ -0,0 +1,5 @@ +package org.example.globalscope; + +public interface Greeter { + String greet(); +} diff --git a/blackbox-global-scope-teardown/src/main/java/org/example/globalscope/ServerStatus.java b/blackbox-global-scope-teardown/src/main/java/org/example/globalscope/ServerStatus.java new file mode 100644 index 000000000..0aaf91915 --- /dev/null +++ b/blackbox-global-scope-teardown/src/main/java/org/example/globalscope/ServerStatus.java @@ -0,0 +1,43 @@ +package org.example.globalscope; + +import java.lang.management.ManagementFactory; + +import javax.management.JMException; +import javax.management.ObjectName; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import jakarta.inject.Singleton; + +/** + * Publishes a management view of this component on the JVM-wide platform MBeanServer, + * the standard JMX pattern (HikariCP pools, Kafka clients, Jetty, and the JDK itself). + */ +@Singleton +public class ServerStatus implements ServerStatusMBean { + + public static final String OBJECT_NAME = "org.example.globalscope:type=ServerStatus"; + + @PostConstruct + void register() { + try { + ManagementFactory.getPlatformMBeanServer().registerMBean(this, new ObjectName(OBJECT_NAME)); + } catch (JMException e) { + throw new IllegalStateException("failed to register " + OBJECT_NAME, e); + } + } + + @PreDestroy + void unregister() { + try { + ManagementFactory.getPlatformMBeanServer().unregisterMBean(new ObjectName(OBJECT_NAME)); + } catch (JMException e) { + throw new IllegalStateException("failed to unregister " + OBJECT_NAME, e); + } + } + + @Override + public String getStatus() { + return "OK"; + } +} diff --git a/blackbox-global-scope-teardown/src/main/java/org/example/globalscope/ServerStatusMBean.java b/blackbox-global-scope-teardown/src/main/java/org/example/globalscope/ServerStatusMBean.java new file mode 100644 index 000000000..fa29e2fa3 --- /dev/null +++ b/blackbox-global-scope-teardown/src/main/java/org/example/globalscope/ServerStatusMBean.java @@ -0,0 +1,7 @@ +package org.example.globalscope; + +/** Standard MBean interface for {@link ServerStatus}. */ +public interface ServerStatusMBean { + + String getStatus(); +} diff --git a/blackbox-global-scope-teardown/src/test/java/org/example/globalscope/T1GlobalTest.java b/blackbox-global-scope-teardown/src/test/java/org/example/globalscope/T1GlobalTest.java new file mode 100644 index 000000000..f3d3e26b3 --- /dev/null +++ b/blackbox-global-scope-teardown/src/test/java/org/example/globalscope/T1GlobalTest.java @@ -0,0 +1,23 @@ +package org.example.globalscope; + +import io.avaje.inject.test.InjectTest; +import jakarta.inject.Inject; +import org.junit.jupiter.api.Test; + +import javax.management.ObjectName; +import java.lang.management.ManagementFactory; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@InjectTest +class T1GlobalTest { + + @Inject ServerStatus serverStatus; + + @Test + void mbeanIsRegistered() throws Exception { + Object status = ManagementFactory.getPlatformMBeanServer() + .getAttribute(new ObjectName(ServerStatus.OBJECT_NAME), "Status"); + assertEquals("OK", status); + } +} diff --git a/blackbox-global-scope-teardown/src/test/java/org/example/globalscope/T2MockTest.java b/blackbox-global-scope-teardown/src/test/java/org/example/globalscope/T2MockTest.java new file mode 100644 index 000000000..c48ff52fa --- /dev/null +++ b/blackbox-global-scope-teardown/src/test/java/org/example/globalscope/T2MockTest.java @@ -0,0 +1,25 @@ +package org.example.globalscope; + +import io.avaje.inject.test.InjectTest; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.when; + +/** + * Never references ServerStatus. Adding one mock of an unrelated interface rebuilds the + * entire application beside the live global scope, so ServerStatus is constructed a second + * time and its duplicate MBean registration fails the wiring. + */ +@InjectTest +class T2MockTest { + + @Mock Greeter greeter; + + @Test + void mockGreeter() { + when(greeter.greet()).thenReturn("mocked"); + assertEquals("mocked", greeter.greet()); + } +} diff --git a/blackbox-global-scope-teardown/src/test/java/org/example/globalscope/T3GlobalAgainTest.java b/blackbox-global-scope-teardown/src/test/java/org/example/globalscope/T3GlobalAgainTest.java new file mode 100644 index 000000000..9bf76e01e --- /dev/null +++ b/blackbox-global-scope-teardown/src/test/java/org/example/globalscope/T3GlobalAgainTest.java @@ -0,0 +1,28 @@ +package org.example.globalscope; + +import io.avaje.inject.test.InjectTest; +import jakarta.inject.Inject; +import org.junit.jupiter.api.Test; + +import javax.management.ObjectName; +import java.lang.management.ManagementFactory; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Same test as T1, back on the shared global scope. Fails after T2: closing the + * failed mock scope ran the duplicate ServerStatus preDestroy, which unregistered + * (by ObjectName) the MBean the still-live global scope instance owns. + */ +@InjectTest +class T3GlobalAgainTest { + + @Inject ServerStatus serverStatus; + + @Test + void mbeanStillRegistered() throws Exception { + Object status = ManagementFactory.getPlatformMBeanServer() + .getAttribute(new ObjectName(ServerStatus.OBJECT_NAME), "Status"); + assertEquals("OK", status); + } +} diff --git a/pom.xml b/pom.xml index bcb853b9f..15dbe1f97 100644 --- a/pom.xml +++ b/pom.xml @@ -55,6 +55,7 @@ blackbox-aspect blackbox-test-inject blackbox-multi-scope + blackbox-global-scope-teardown