Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

4.x: Fail HelidonTests by default when pinning jfr event is detected #8877

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* 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.testing.junit5;

import java.util.ArrayList;
import java.util.List;

import jdk.jfr.consumer.RecordedEvent;
import jdk.jfr.consumer.RecordedFrame;
import jdk.jfr.consumer.RecordingStream;
import org.junit.jupiter.api.extension.AfterAllCallback;
import org.junit.jupiter.api.extension.BeforeAllCallback;
import org.junit.jupiter.api.extension.ExtensionContext;

import static org.junit.jupiter.api.Assertions.fail;

/**
* JUnit5 extension to support pinned threads validation.
*/
class HelidonPinnedThreadValidationJunitExtension implements BeforeAllCallback, AfterAllCallback {

private List<EventWrapper> jfrVTPinned;
private RecordingStream recordingStream;
private boolean pinnedThreadValidation;

@Override
public void beforeAll(ExtensionContext context) throws Exception {
Class<?> testClass = context.getRequiredTestClass();
pinnedThreadValidation = testClass.getAnnotation(PinnedThreadValidation.class) != null;
if (pinnedThreadValidation) {
jfrVTPinned = new ArrayList<>();
recordingStream = new RecordingStream();
recordingStream.enable("jdk.VirtualThreadPinned").withStackTrace();
recordingStream.onEvent("jdk.VirtualThreadPinned", event -> {
jfrVTPinned.add(new EventWrapper(event));
});
recordingStream.startAsync();
}
}

@Override
public void afterAll(ExtensionContext context) {
if (pinnedThreadValidation) {
try {
// Flush ending events
recordingStream.stop();
if (!jfrVTPinned.isEmpty()) {
fail("Some pinned virtual threads were detected:\n" + jfrVTPinned);
}
} finally {
recordingStream.close();
}
}
}

private static class EventWrapper {

private final RecordedEvent recordedEvent;

private EventWrapper(RecordedEvent recordedEvent) {
this.recordedEvent = recordedEvent;
}

@Override
public String toString() {
StringBuilder builder = new StringBuilder(recordedEvent.toString());
if (recordedEvent.getStackTrace() != null) {
builder.append("full-stackTrace = [");
List<RecordedFrame> 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();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* 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.testing.junit5;

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;

import org.junit.jupiter.api.extension.ExtendWith;

/**
* An annotation making this test class to fail at the end if a pinned virtual thread was detected.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@ExtendWith(HelidonPinnedThreadValidationJunitExtension.class)
@Inherited
public @interface PinnedThreadValidation {
}
1 change: 1 addition & 0 deletions microprofile/testing/junit5/src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import jakarta.ws.rs.client.ClientBuilder;
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;
Expand All @@ -68,6 +71,8 @@
import org.testng.ITestResult;
import org.testng.annotations.Test;

import static org.testng.Assert.fail;

/**
* TestNG extension to support Helidon CDI container in tests.
*/
Expand All @@ -85,8 +90,12 @@ public class HelidonTestNgListener implements IClassListener, ITestListener {
private List<AddExtension> classLevelExtensions = new ArrayList<>();
private List<AddBean> classLevelBeans = new ArrayList<>();
private ConfigMeta classLevelConfigMeta = new ConfigMeta();
private List<EventWrapper> jfrVTPinned;
private RecordingStream recordingStream;
private boolean classLevelDisableDiscovery = false;
private boolean resetPerTest;
private boolean pinnedThreadValidation;

private Class<?> testClass;
private Object testInstance;
private ConfigProviderResolver configProviderResolver;
Expand All @@ -97,6 +106,9 @@ public class HelidonTestNgListener implements IClassListener, ITestListener {
public void onBeforeClass(ITestClass iTestClass) {
testClass = iTestClass.getRealClass();

pinnedThreadValidation = testClass.getAnnotation(PinnedThreadValidation.class) != null;
startRecordingStream();

AddConfig[] configs = getAnnotations(testClass, AddConfig.class);
classLevelConfigMeta.addConfig(configs);
classLevelConfigMeta.configuration(testClass.getAnnotation(Configuration.class));
Expand Down Expand Up @@ -147,16 +159,19 @@ public void onBeforeClass(ITestClass iTestClass) {
}
}


@Override
public void onAfterClass(ITestClass testClass) {
if (!resetPerTest) {
releaseConfig();
stopContainer();
}
closeRecordingStream();
}

@Override
public void onTestStart(ITestResult result) {

if (resetPerTest) {
Method method = result.getMethod().getConstructorOrMethod().getMethod();
AddConfig[] configs = method.getAnnotationsByType(AddConfig.class);
Expand Down Expand Up @@ -229,7 +244,7 @@ private void validatePerTest() {
}
validateFields(testClass.getFields());
validateFields(testClass.getDeclaredFields());
}
}

private void configure(ConfigMeta configMeta) {
if (config != null) {
Expand Down Expand Up @@ -268,6 +283,7 @@ private void releaseConfig() {
if (configProviderResolver != null && config != null) {
configProviderResolver.releaseConfig(config);
config = null;

classLevelExtensions = new ArrayList<>();
classLevelBeans = new ArrayList<>();
classLevelConfigMeta = new ConfigMeta();
Expand Down Expand Up @@ -312,6 +328,32 @@ private void stopContainer() {
}
}

private void startRecordingStream() {
if (pinnedThreadValidation) {
jfrVTPinned = new ArrayList<>();
recordingStream = new RecordingStream();
recordingStream.enable("jdk.VirtualThreadPinned").withStackTrace();
recordingStream.onEvent("jdk.VirtualThreadPinned", event -> {
jfrVTPinned.add(new EventWrapper(event));
});
recordingStream.startAsync();
}
}

private void closeRecordingStream() {
if (pinnedThreadValidation) {
try {
// Flush ending events
recordingStream.stop();
if (!jfrVTPinned.isEmpty()) {
fail("Some pinned virtual threads were detected:\n" + jfrVTPinned);
}
} finally {
recordingStream.close();
}
}
}

@SuppressWarnings("unchecked")
private <T extends Annotation> T[] getAnnotations(Class<?> testClass, Class<T> annotClass) {
// inherited does not help, as it only returns annot from superclass if
Expand Down Expand Up @@ -406,7 +448,7 @@ void registerOtherBeans(@Observes AfterBeanDiscovery event) {
.addType(jakarta.ws.rs.client.WebTarget.class)
.scope(ApplicationScoped.class)
.produceWith(context -> ClientBuilder.newClient().target(clientUri()));
}
}

void registerAddedBeans(@Observes BeforeBeanDiscovery event) {
for (AddBean beanDef : addBeans) {
Expand Down Expand Up @@ -461,12 +503,12 @@ public void inject(T testInstance, CreationalContext<T> cc) {
@Override
public void postConstruct(T testInstance) {
delegate.postConstruct(testInstance);
}
}

@Override
public void preDestroy(T testInstance) {
delegate.preDestroy(testInstance);
}
}

@Override
public T produce(CreationalContext<T> cc) {
Expand Down Expand Up @@ -521,10 +563,12 @@ void addConfigBlock(AddConfigBlock config) {

ConfigMeta nextMethod() {
ConfigMeta methodMeta = new ConfigMeta();

methodMeta.additionalKeys.putAll(this.additionalKeys);
methodMeta.additionalSources.addAll(this.additionalSources);
methodMeta.useExisting = this.useExisting;
methodMeta.profile = this.profile;

return methodMeta;
}
}
Expand Down Expand Up @@ -594,4 +638,30 @@ public Class<? extends Extension> value() {
private static final class SingletonLiteral extends AnnotationLiteral<Singleton> implements Singleton {
static final SingletonLiteral INSTANCE = new SingletonLiteral();
}

private static class EventWrapper {

private final RecordedEvent recordedEvent;

private EventWrapper(RecordedEvent recordedEvent) {
this.recordedEvent = recordedEvent;
}

@Override
public String toString() {
StringBuilder builder = new StringBuilder(recordedEvent.toString());
if (recordedEvent.getStackTrace() != null) {
builder.append("full-stackTrace = [");
List<RecordedFrame> 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();
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* 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.testing.testng;

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;

/**
* An annotation making this test class to fail at the end if a pinned virtual thread was detected.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Inherited
public @interface PinnedThreadValidation {
}
1 change: 1 addition & 0 deletions microprofile/testing/testng/src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
requires jakarta.cdi;
requires jakarta.inject;
requires jakarta.ws.rs;
requires jdk.jfr;
requires microprofile.config.api;
requires org.testng;

Expand Down
Original file line number Diff line number Diff line change
@@ -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.PinnedThreadValidation;

import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;

@PinnedThreadValidation
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();
}
}
Loading