diff --git a/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/HelidonJunitExtension.java b/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/HelidonJunitExtension.java index 5599a1bc9fe..669424f0dfd 100644 --- a/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/HelidonJunitExtension.java +++ b/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/HelidonJunitExtension.java @@ -59,6 +59,9 @@ import jakarta.ws.rs.client.Client; import jakarta.ws.rs.client.ClientBuilder; import jakarta.ws.rs.client.WebTarget; +import jdk.jfr.consumer.RecordedEvent; +import jdk.jfr.consumer.RecordedFrame; +import jdk.jfr.consumer.RecordingStream; import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.config.spi.ConfigBuilder; import org.eclipse.microprofile.config.spi.ConfigProviderResolver; @@ -77,6 +80,7 @@ import org.junit.jupiter.api.extension.ParameterResolver; import org.junit.jupiter.api.extension.ReflectiveInvocationContext; +import static org.junit.jupiter.api.Assertions.fail; /** * Junit5 extension to support Helidon CDI container in tests. @@ -100,7 +104,9 @@ class HelidonJunitExtension implements BeforeAllCallback, private final List classLevelExtensions = new ArrayList<>(); private final List classLevelBeans = new ArrayList<>(); + private final List jfrEvents = new ArrayList<>(); private final ConfigMeta classLevelConfigMeta = new ConfigMeta(); + private final RecordingStream recordingStream = new RecordingStream(); private boolean classLevelDisableDiscovery = false; private boolean resetPerTest; @@ -113,6 +119,12 @@ class HelidonJunitExtension implements BeforeAllCallback, @SuppressWarnings("unchecked") @Override public void beforeAll(ExtensionContext context) { + recordingStream.enable("jdk.VirtualThreadPinned").withStackTrace(); + recordingStream.onEvent("jdk.VirtualThreadPinned", event -> { + jfrEvents.add(new EventWrapper(event)); + }); + recordingStream.startAsync(); + testClass = context.getRequiredTestClass(); AddConfig[] configs = getAnnotations(testClass, AddConfig.class); @@ -383,6 +395,19 @@ public void afterAll(ExtensionContext context) { stopContainer(); releaseConfig(); callAfterStop(); + closeRecordingStream(); + } + + private void closeRecordingStream() { + try { + // Flush ending events + recordingStream.stop(); + if (!jfrEvents.isEmpty()) { + fail("Some pinned virtual threads were detected:\n" + jfrEvents); + } + } finally { + recordingStream.close(); + } } @Override @@ -748,4 +773,30 @@ public Class value() { } } + private static class EventWrapper { + + private final RecordedEvent recordedEvent; + + private EventWrapper(RecordedEvent recordedEvent) { + this.recordedEvent = recordedEvent; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder() + .append("\nstartTime = ").append(recordedEvent.getStartTime()) + .append("\nduration = ").append(recordedEvent.getDuration()); + if (recordedEvent.getStackTrace() != null) { + builder.append("\nstackTrace = ["); + List frames = recordedEvent.getStackTrace().getFrames(); + for (RecordedFrame frame : frames) { + builder.append("\n\t").append(frame.getMethod().getType().getName()) + .append("#").append(frame.getMethod().getName()) + .append("(").append(frame.getLineNumber()).append(")"); + } + builder.append("\n]"); + } + return builder.toString(); + } + } } diff --git a/microprofile/testing/junit5/src/main/java/module-info.java b/microprofile/testing/junit5/src/main/java/module-info.java index f9514942be0..e20192d5d8a 100644 --- a/microprofile/testing/junit5/src/main/java/module-info.java +++ b/microprofile/testing/junit5/src/main/java/module-info.java @@ -24,6 +24,7 @@ requires io.helidon.microprofile.cdi; requires jakarta.inject; requires org.junit.jupiter.api; + requires jdk.jfr; requires transitive jakarta.cdi; requires transitive jakarta.ws.rs; diff --git a/microprofile/tests/testing/junit5/src/test/java/io/helidon/microprofile/tests/testing/junit5/TestPinnedThread.java b/microprofile/tests/testing/junit5/src/test/java/io/helidon/microprofile/tests/testing/junit5/TestPinnedThread.java new file mode 100644 index 00000000000..17ee366aff9 --- /dev/null +++ b/microprofile/tests/testing/junit5/src/test/java/io/helidon/microprofile/tests/testing/junit5/TestPinnedThread.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed 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 io.helidon.microprofile.tests.testing.junit5; + +import io.helidon.microprofile.testing.junit5.HelidonTest; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +@HelidonTest +class TestPinnedThread { + + @Test + @Disabled("Enable to verify pinned threads fails") + void test() throws InterruptedException { + Thread.ofVirtual().start(() -> { + synchronized (this) { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + Thread.interrupted(); + } + } + }).join(); + } +}