diff --git a/independent-projects/junit5-virtual-threads/pom.xml b/independent-projects/junit5-virtual-threads/pom.xml
index 875e37d03ee88f..c999a9bebfb93e 100644
--- a/independent-projects/junit5-virtual-threads/pom.xml
+++ b/independent-projects/junit5-virtual-threads/pom.xml
@@ -46,6 +46,7 @@
1.11.0
5.10.3
+ 1.10.3
3.26.3
@@ -57,6 +58,11 @@
compile
${junit.jupiter.version}
+
+ org.junit.platform
+ junit-platform-testkit
+ ${junit.testkit.version}
+
org.assertj
assertj-core
@@ -135,6 +141,9 @@
-Djava.io.tmpdir="${project.build.directory}"
MAVEN_OPTS
+
+ io.quarkus.test.junit5.virtual.internal.ignore.**Test.java
+
diff --git a/independent-projects/junit5-virtual-threads/src/main/java/io/quarkus/test/junit5/virtual/ShouldNotPin.java b/independent-projects/junit5-virtual-threads/src/main/java/io/quarkus/test/junit5/virtual/ShouldNotPin.java
index b6ec800d78d984..eb2f3a2596b80a 100644
--- a/independent-projects/junit5-virtual-threads/src/main/java/io/quarkus/test/junit5/virtual/ShouldNotPin.java
+++ b/independent-projects/junit5-virtual-threads/src/main/java/io/quarkus/test/junit5/virtual/ShouldNotPin.java
@@ -1,6 +1,7 @@
package io.quarkus.test.junit5.virtual;
import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@@ -12,6 +13,7 @@
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.TYPE })
+@Inherited
public @interface ShouldNotPin {
/**
diff --git a/independent-projects/junit5-virtual-threads/src/main/java/io/quarkus/test/junit5/virtual/ShouldPin.java b/independent-projects/junit5-virtual-threads/src/main/java/io/quarkus/test/junit5/virtual/ShouldPin.java
index d98f5166a8c3a6..84b2800fb95f33 100644
--- a/independent-projects/junit5-virtual-threads/src/main/java/io/quarkus/test/junit5/virtual/ShouldPin.java
+++ b/independent-projects/junit5-virtual-threads/src/main/java/io/quarkus/test/junit5/virtual/ShouldPin.java
@@ -1,6 +1,7 @@
package io.quarkus.test.junit5.virtual;
import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@@ -12,6 +13,7 @@
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.TYPE })
+@Inherited
public @interface ShouldPin {
int atMost() default Integer.MAX_VALUE;
diff --git a/independent-projects/junit5-virtual-threads/src/test/java/io/quarkus/test/junit5/virtual/internal/JUnitEngine.java b/independent-projects/junit5-virtual-threads/src/test/java/io/quarkus/test/junit5/virtual/internal/JUnitEngine.java
new file mode 100644
index 00000000000000..1da2015711a4bb
--- /dev/null
+++ b/independent-projects/junit5-virtual-threads/src/test/java/io/quarkus/test/junit5/virtual/internal/JUnitEngine.java
@@ -0,0 +1,34 @@
+package io.quarkus.test.junit5.virtual.internal;
+
+import static org.junit.platform.engine.discovery.DiscoverySelectors.selectMethod;
+import static org.junit.platform.testkit.engine.EventConditions.*;
+
+import org.assertj.core.api.Condition;
+import org.junit.platform.testkit.engine.EngineTestKit;
+import org.junit.platform.testkit.engine.Events;
+
+public class JUnitEngine {
+
+ public static void runTestAndAssertFailure(Class> clazz, String methodName, String message) {
+ runTest(clazz, methodName).assertThatEvents()
+ .haveExactly(1, event(test(methodName),
+ finishedWithFailure(new Condition<>(
+ throwable -> throwable instanceof AssertionError && throwable.getMessage().contains(message),
+ ""))));
+ }
+
+ public static void runTestAndAssertSuccess(Class> clazz, String methodName) {
+ runTest(clazz, methodName).assertThatEvents()
+ .haveExactly(1, event(test(methodName),
+ finishedSuccessfully()));
+ }
+
+ public static Events runTest(Class> clazz, String methodName) {
+ return EngineTestKit
+ .engine("junit-jupiter")
+ .selectors(selectMethod(clazz, methodName))
+ .execute()
+ .testEvents();
+ }
+
+}
diff --git a/independent-projects/junit5-virtual-threads/src/test/java/io/quarkus/test/junit5/virtual/internal/LoomUnitExampleOnClassTest.java b/independent-projects/junit5-virtual-threads/src/test/java/io/quarkus/test/junit5/virtual/internal/LoomUnitExampleOnClassTest.java
deleted file mode 100644
index 470343dd01067f..00000000000000
--- a/independent-projects/junit5-virtual-threads/src/test/java/io/quarkus/test/junit5/virtual/internal/LoomUnitExampleOnClassTest.java
+++ /dev/null
@@ -1,33 +0,0 @@
-package io.quarkus.test.junit5.virtual.internal;
-
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.condition.EnabledForJreRange;
-import org.junit.jupiter.api.condition.JRE;
-
-import io.quarkus.test.junit5.virtual.ShouldNotPin;
-import io.quarkus.test.junit5.virtual.ShouldPin;
-import io.quarkus.test.junit5.virtual.VirtualThreadUnit;
-
-@VirtualThreadUnit
-@ShouldNotPin // You can use @ShouldNotPin or @ShouldPin on the class itself, it's applied to each method.
-public class LoomUnitExampleOnClassTest {
-
- @Test
- public void testThatShouldNotPin() {
- // ...
- }
-
- @Test
- @ShouldPin(atMost = 1) // Method annotation overrides the class annotation
- @EnabledForJreRange(min = JRE.JAVA_21)
- public void testThatShouldPinAtMostOnce() {
- TestPinJfrEvent.pin();
- }
-
- @Test
- @ShouldNotPin(atMost = 1) // Method annotation overrides the class annotation
- public void testThatShouldNotPinAtMostOnce() {
- TestPinJfrEvent.pin();
- }
-
-}
diff --git a/independent-projects/junit5-virtual-threads/src/test/java/io/quarkus/test/junit5/virtual/internal/ShouldNotPinTest.java b/independent-projects/junit5-virtual-threads/src/test/java/io/quarkus/test/junit5/virtual/internal/ShouldNotPinTest.java
new file mode 100644
index 00000000000000..3105b2d3eefeb9
--- /dev/null
+++ b/independent-projects/junit5-virtual-threads/src/test/java/io/quarkus/test/junit5/virtual/internal/ShouldNotPinTest.java
@@ -0,0 +1,46 @@
+package io.quarkus.test.junit5.virtual.internal;
+
+import static io.quarkus.test.junit5.virtual.internal.JUnitEngine.runTestAndAssertFailure;
+import static io.quarkus.test.junit5.virtual.internal.JUnitEngine.runTestAndAssertSuccess;
+import static org.junit.jupiter.params.provider.Arguments.arguments;
+
+import java.util.stream.Stream;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledForJreRange;
+import org.junit.jupiter.api.condition.JRE;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import io.quarkus.test.junit5.virtual.internal.ignore.LoomUnitExampleOnMethodTest;
+import io.quarkus.test.junit5.virtual.internal.ignore.LoomUnitExampleShouldNotPinOnClassTest;
+import io.quarkus.test.junit5.virtual.internal.ignore.LoomUnitExampleShouldNotPinOnSuperClassTest;
+import io.quarkus.test.junit5.virtual.internal.ignore.LoomUnitExampleShouldPinOnSuperClassTest;
+
+public class ShouldNotPinTest {
+
+ @ParameterizedTest
+ @MethodSource
+ @EnabledForJreRange(min = JRE.JAVA_21)
+ void testShouldNotPinButPinEventDetected(Class> clazz, String methodName) {
+ runTestAndAssertFailure(clazz, methodName, "was expected to NOT pin the carrier thread");
+ }
+
+ public static Stream testShouldNotPinButPinEventDetected() {
+ return Stream.of(
+ arguments(LoomUnitExampleOnMethodTest.class, "failWhenShouldNotPinAndPinDetected"),
+ arguments(LoomUnitExampleOnMethodTest.class, "failWhenShouldNotPinAtMostAndTooManyPinDetected"),
+ arguments(LoomUnitExampleShouldNotPinOnClassTest.class, "failWhenShouldNotPinAndPinDetected"),
+ arguments(LoomUnitExampleShouldNotPinOnSuperClassTest.class, "failWhenShouldNotPinAndPinDetected"),
+ arguments(LoomUnitExampleShouldPinOnSuperClassTest.class, "failWhenShouldNotPinAndPinDetected"));
+ }
+
+ @Test
+ @EnabledForJreRange(min = JRE.JAVA_21)
+ void shouldNotPinOnMethodOverridesClassAnnotation() {
+ runTestAndAssertSuccess(LoomUnitExampleShouldNotPinOnClassTest.class, "overrideClassAnnotation");
+ runTestAndAssertSuccess(LoomUnitExampleShouldNotPinOnSuperClassTest.class, "overrideClassAnnotation");
+ }
+
+}
diff --git a/independent-projects/junit5-virtual-threads/src/test/java/io/quarkus/test/junit5/virtual/internal/ShouldPinTest.java b/independent-projects/junit5-virtual-threads/src/test/java/io/quarkus/test/junit5/virtual/internal/ShouldPinTest.java
new file mode 100644
index 00000000000000..346c5ea5aedb47
--- /dev/null
+++ b/independent-projects/junit5-virtual-threads/src/test/java/io/quarkus/test/junit5/virtual/internal/ShouldPinTest.java
@@ -0,0 +1,36 @@
+package io.quarkus.test.junit5.virtual.internal;
+
+import static io.quarkus.test.junit5.virtual.internal.JUnitEngine.runTestAndAssertFailure;
+import static org.junit.jupiter.params.provider.Arguments.arguments;
+
+import java.util.stream.Stream;
+
+import org.junit.jupiter.api.condition.EnabledForJreRange;
+import org.junit.jupiter.api.condition.JRE;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import io.quarkus.test.junit5.virtual.internal.ignore.LoomUnitExampleOnMethodTest;
+import io.quarkus.test.junit5.virtual.internal.ignore.LoomUnitExampleShouldNotPinOnClassTest;
+import io.quarkus.test.junit5.virtual.internal.ignore.LoomUnitExampleShouldNotPinOnSuperClassTest;
+import io.quarkus.test.junit5.virtual.internal.ignore.LoomUnitExampleShouldPinOnSuperClassTest;
+
+public class ShouldPinTest {
+
+ @ParameterizedTest
+ @MethodSource
+ @EnabledForJreRange(min = JRE.JAVA_21)
+ void testShouldPinButNoPinEventDetected(Class> clazz, String methodName) {
+ runTestAndAssertFailure(clazz, methodName, "was expected to pin the carrier thread, it didn't");
+ }
+
+ public static Stream testShouldPinButNoPinEventDetected() {
+ return Stream.of(
+ arguments(LoomUnitExampleOnMethodTest.class, "failWhenMethodShouldPinButNoPinDetected"),
+ arguments(LoomUnitExampleShouldNotPinOnClassTest.class, "failWhenMethodShouldPinButNoPinDetected"),
+ arguments(LoomUnitExampleShouldNotPinOnSuperClassTest.class, "failWhenShouldPinAndNoPinDetected"),
+ arguments(LoomUnitExampleShouldPinOnSuperClassTest.class, "failWhenShouldPinAndNoPinDetected"));
+ }
+
+}
diff --git a/independent-projects/junit5-virtual-threads/src/test/java/io/quarkus/test/junit5/virtual/internal/TestPinJfrEvent.java b/independent-projects/junit5-virtual-threads/src/test/java/io/quarkus/test/junit5/virtual/internal/TestPinJfrEvent.java
index 1c56fb259ae207..27ed1bc43e7578 100644
--- a/independent-projects/junit5-virtual-threads/src/test/java/io/quarkus/test/junit5/virtual/internal/TestPinJfrEvent.java
+++ b/independent-projects/junit5-virtual-threads/src/test/java/io/quarkus/test/junit5/virtual/internal/TestPinJfrEvent.java
@@ -17,7 +17,7 @@ public TestPinJfrEvent(String message) {
this.message = message;
}
- static void pin() {
+ public static void pin() {
TestPinJfrEvent event = new TestPinJfrEvent("Hello, JFR!");
event.commit();
}
diff --git a/independent-projects/junit5-virtual-threads/src/test/java/io/quarkus/test/junit5/virtual/internal/ignore/LoomUnitExampleOnMethodTest.java b/independent-projects/junit5-virtual-threads/src/test/java/io/quarkus/test/junit5/virtual/internal/ignore/LoomUnitExampleOnMethodTest.java
new file mode 100644
index 00000000000000..84623f4723aa86
--- /dev/null
+++ b/independent-projects/junit5-virtual-threads/src/test/java/io/quarkus/test/junit5/virtual/internal/ignore/LoomUnitExampleOnMethodTest.java
@@ -0,0 +1,31 @@
+package io.quarkus.test.junit5.virtual.internal.ignore;
+
+import org.junit.jupiter.api.Test;
+
+import io.quarkus.test.junit5.virtual.ShouldNotPin;
+import io.quarkus.test.junit5.virtual.ShouldPin;
+import io.quarkus.test.junit5.virtual.VirtualThreadUnit;
+import io.quarkus.test.junit5.virtual.internal.TestPinJfrEvent;
+
+@VirtualThreadUnit
+public class LoomUnitExampleOnMethodTest {
+
+ @Test
+ @ShouldNotPin
+ void failWhenShouldNotPinAndPinDetected() {
+ TestPinJfrEvent.pin();
+ }
+
+ @Test
+ @ShouldNotPin(atMost = 1)
+ void failWhenShouldNotPinAtMostAndTooManyPinDetected() {
+ TestPinJfrEvent.pin();
+ TestPinJfrEvent.pin();
+ }
+
+ @Test
+ @ShouldPin
+ void failWhenMethodShouldPinButNoPinDetected() {
+ }
+
+}
diff --git a/independent-projects/junit5-virtual-threads/src/test/java/io/quarkus/test/junit5/virtual/internal/ignore/LoomUnitExampleShouldNotPinOnClassTest.java b/independent-projects/junit5-virtual-threads/src/test/java/io/quarkus/test/junit5/virtual/internal/ignore/LoomUnitExampleShouldNotPinOnClassTest.java
new file mode 100644
index 00000000000000..5fef87cd94d099
--- /dev/null
+++ b/independent-projects/junit5-virtual-threads/src/test/java/io/quarkus/test/junit5/virtual/internal/ignore/LoomUnitExampleShouldNotPinOnClassTest.java
@@ -0,0 +1,31 @@
+package io.quarkus.test.junit5.virtual.internal.ignore;
+
+import org.junit.jupiter.api.Test;
+
+import io.quarkus.test.junit5.virtual.ShouldNotPin;
+import io.quarkus.test.junit5.virtual.ShouldPin;
+import io.quarkus.test.junit5.virtual.VirtualThreadUnit;
+import io.quarkus.test.junit5.virtual.internal.TestPinJfrEvent;
+
+@VirtualThreadUnit
+@ShouldNotPin // You can use @ShouldNotPin or @ShouldPin on the class itself, it's applied to each method.
+public class LoomUnitExampleShouldNotPinOnClassTest {
+
+ @Test
+ public void failWhenShouldNotPinAndPinDetected() {
+ TestPinJfrEvent.pin();
+ }
+
+ @Test
+ @ShouldPin(atMost = 1)
+ public void overrideClassAnnotation() {
+ TestPinJfrEvent.pin();
+ }
+
+ @Test
+ @ShouldPin
+ public void failWhenMethodShouldPinButNoPinDetected() {
+
+ }
+
+}
diff --git a/independent-projects/junit5-virtual-threads/src/test/java/io/quarkus/test/junit5/virtual/internal/ignore/LoomUnitExampleShouldNotPinOnSuperClass.java b/independent-projects/junit5-virtual-threads/src/test/java/io/quarkus/test/junit5/virtual/internal/ignore/LoomUnitExampleShouldNotPinOnSuperClass.java
new file mode 100644
index 00000000000000..4741e9e31cd5b6
--- /dev/null
+++ b/independent-projects/junit5-virtual-threads/src/test/java/io/quarkus/test/junit5/virtual/internal/ignore/LoomUnitExampleShouldNotPinOnSuperClass.java
@@ -0,0 +1,9 @@
+package io.quarkus.test.junit5.virtual.internal.ignore;
+
+import io.quarkus.test.junit5.virtual.ShouldNotPin;
+import io.quarkus.test.junit5.virtual.VirtualThreadUnit;
+
+@VirtualThreadUnit
+@ShouldNotPin // You can use @ShouldNotPin or @ShouldPin on the super class itself, it's applied to each method.
+public abstract class LoomUnitExampleShouldNotPinOnSuperClass {
+}
diff --git a/independent-projects/junit5-virtual-threads/src/test/java/io/quarkus/test/junit5/virtual/internal/ignore/LoomUnitExampleShouldNotPinOnSuperClassTest.java b/independent-projects/junit5-virtual-threads/src/test/java/io/quarkus/test/junit5/virtual/internal/ignore/LoomUnitExampleShouldNotPinOnSuperClassTest.java
new file mode 100644
index 00000000000000..25b2cc82e521fa
--- /dev/null
+++ b/independent-projects/junit5-virtual-threads/src/test/java/io/quarkus/test/junit5/virtual/internal/ignore/LoomUnitExampleShouldNotPinOnSuperClassTest.java
@@ -0,0 +1,27 @@
+package io.quarkus.test.junit5.virtual.internal.ignore;
+
+import org.junit.jupiter.api.Test;
+
+import io.quarkus.test.junit5.virtual.ShouldPin;
+import io.quarkus.test.junit5.virtual.internal.TestPinJfrEvent;
+
+public class LoomUnitExampleShouldNotPinOnSuperClassTest extends LoomUnitExampleShouldNotPinOnSuperClass {
+
+ @Test
+ public void failWhenShouldNotPinAndPinDetected() {
+ TestPinJfrEvent.pin();
+ }
+
+ @Test
+ @ShouldPin(atMost = 1)
+ public void overrideClassAnnotation() {
+ TestPinJfrEvent.pin();
+ }
+
+ @Test
+ @ShouldPin // Method annotation overrides the class annotation
+ public void failWhenShouldPinAndNoPinDetected() {
+
+ }
+
+}
diff --git a/independent-projects/junit5-virtual-threads/src/test/java/io/quarkus/test/junit5/virtual/internal/ignore/LoomUnitExampleShouldPinOnSuperClass.java b/independent-projects/junit5-virtual-threads/src/test/java/io/quarkus/test/junit5/virtual/internal/ignore/LoomUnitExampleShouldPinOnSuperClass.java
new file mode 100644
index 00000000000000..4cc571619671a5
--- /dev/null
+++ b/independent-projects/junit5-virtual-threads/src/test/java/io/quarkus/test/junit5/virtual/internal/ignore/LoomUnitExampleShouldPinOnSuperClass.java
@@ -0,0 +1,9 @@
+package io.quarkus.test.junit5.virtual.internal.ignore;
+
+import io.quarkus.test.junit5.virtual.ShouldPin;
+import io.quarkus.test.junit5.virtual.VirtualThreadUnit;
+
+@VirtualThreadUnit
+@ShouldPin // You can use @ShouldNotPin or @ShouldPin on the super class itself, it's applied to each method.
+public abstract class LoomUnitExampleShouldPinOnSuperClass {
+}
diff --git a/independent-projects/junit5-virtual-threads/src/test/java/io/quarkus/test/junit5/virtual/internal/ignore/LoomUnitExampleShouldPinOnSuperClassTest.java b/independent-projects/junit5-virtual-threads/src/test/java/io/quarkus/test/junit5/virtual/internal/ignore/LoomUnitExampleShouldPinOnSuperClassTest.java
new file mode 100644
index 00000000000000..322b024b1a88d4
--- /dev/null
+++ b/independent-projects/junit5-virtual-threads/src/test/java/io/quarkus/test/junit5/virtual/internal/ignore/LoomUnitExampleShouldPinOnSuperClassTest.java
@@ -0,0 +1,21 @@
+package io.quarkus.test.junit5.virtual.internal.ignore;
+
+import org.junit.jupiter.api.Test;
+
+import io.quarkus.test.junit5.virtual.ShouldNotPin;
+import io.quarkus.test.junit5.virtual.internal.TestPinJfrEvent;
+
+public class LoomUnitExampleShouldPinOnSuperClassTest extends LoomUnitExampleShouldPinOnSuperClass {
+
+ @Test
+ @ShouldNotPin // Method annotation overrides the class annotation
+ public void failWhenShouldNotPinAndPinDetected() {
+ TestPinJfrEvent.pin();
+ }
+
+ @Test
+ public void failWhenShouldPinAndNoPinDetected() {
+
+ }
+
+}