Skip to content

Commit

Permalink
Integrate pinning detection to HelidonTest
Browse files Browse the repository at this point in the history
  • Loading branch information
danielkec committed Dec 13, 2024
1 parent 89198a6 commit eb33db3
Show file tree
Hide file tree
Showing 5 changed files with 180 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

Expand Down Expand Up @@ -113,7 +114,7 @@ class HelidonJunitExtension implements BeforeAllCallback,
private ConfigProviderResolver configProviderResolver;
private Config config;
private SeContainer container;
private PinningRecorder pinningRecorder = new PinningRecorder();
private PinningRecorder pinningRecorder;// = new PinningRecorder();


@SuppressWarnings("unchecked")
Expand All @@ -140,7 +141,12 @@ public void beforeAll(ExtensionContext context) {
resetPerTest = testAnnot.resetPerTest();
}

pinningRecorder.record(Duration.ofMillis(20));
pinningRecorder = new PinningRecorder();
if (testAnnot.pinningDetection()) {
pinningRecorder.record(Duration.ofMillis(Optional.ofNullable(testAnnot)
.map(HelidonTest::pinningThreshold)
.orElse(20L)));
}

DisableDiscovery discovery = getAnnotation(testClass, DisableDiscovery.class, metaAnnotations);
if (discovery != null) {
Expand Down Expand Up @@ -273,7 +279,7 @@ public void afterEach(ExtensionContext context) throws Exception {
releaseConfig();
stopContainer();
}
pinningRecorder.checkAndThrow();
// pinningRecorder.checkAndThrow();
}

private void validatePerClass() {
Expand Down Expand Up @@ -435,6 +441,7 @@ public void afterAll(ExtensionContext context) {
callAfterStop();
pinningRecorder.close();
pinningRecorder.checkAndThrow();
pinningRecorder = null;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

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


/**
* An annotation making this test class a CDI bean with support for injection.
* <p>
Expand Down Expand Up @@ -52,4 +53,6 @@
* @return whether to reset container per test method
*/
boolean resetPerTest() default false;
long pinningThreshold() default 20;
boolean pinningDetection() default true;
}
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,6 @@ public void onBeforeClass(ITestClass iTestClass) {
pinnedThreadValidation = testClass.getAnnotation(PinnedThreadValidation.class) != null;
startRecordingStream();

AddConfig[] configs = getAnnotations(testClass, AddConfig.class);
classLevelConfigMeta.addConfig(configs);
classLevelConfigMeta.configuration(getAnnotation(testClass, Configuration.class, metaAnnotations));
classLevelConfigMeta.addConfigBlock(getAnnotation(testClass, AddConfigBlock.class, metaAnnotations));
Expand Down Expand Up @@ -336,7 +335,7 @@ private List<Annotation> extractMetaAnnotations(Class<?> testClass) {
for (Annotation testAnnotation : testAnnotations) {
List<Annotation> annotations = List.of(testAnnotation.annotationType().getAnnotations());
List<Class<?>> annotationsClass = annotations.stream()
.map(a -> a.annotationType()).collect(Collectors.toList());
.map(Annotation::annotationType).collect(Collectors.toList());
if (!Collections.disjoint(TEST_ANNOTATIONS, annotationsClass)) {
// Contains at least one of HELIDON_TEST_ANNOTATIONS
return annotations;
Expand Down
7 changes: 7 additions & 0 deletions microprofile/tests/testing/junit5/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -64,5 +64,12 @@
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-testkit</artifactId>
<version>1.11.3</version>
<scope>test</scope>
</dependency>

</dependencies>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,172 @@

package io.helidon.microprofile.tests.testing.junit5;

import io.helidon.microprofile.testing.junit5.PinnedThreadValidation;
import java.util.Arrays;

import org.junit.jupiter.api.Disabled;
import io.helidon.microprofile.testing.common.PinningException;
import io.helidon.microprofile.testing.junit5.AddBean;
import io.helidon.microprofile.testing.junit5.HelidonTest;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.client.WebTarget;
import org.assertj.core.api.Condition;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.platform.testkit.engine.EngineTestKit;
import org.junit.platform.testkit.engine.Event;
import org.junit.platform.testkit.engine.Events;

import static org.junit.platform.commons.util.FunctionUtils.where;
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
import static org.junit.platform.testkit.engine.EventConditions.displayName;
import static org.junit.platform.testkit.engine.EventConditions.event;
import static org.junit.platform.testkit.engine.EventConditions.finishedWithFailure;
import static org.junit.platform.testkit.engine.TestExecutionResultConditions.instanceOf;
import static org.junit.platform.testkit.engine.TestExecutionResultConditions.message;

@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();
void engineTest() {
Events events = EngineTestKit.engine("junit-jupiter")
.selectors(
selectClass(PinningTestCase.class),
selectClass(PinningExtraThreadTestCase.class),
selectClass(NoPinningTestCase.class),
selectClass(NoPinningExtraThreadTestCase.class)
)
.execute()
.containerEvents()
.assertStatistics(stats -> stats
.failed(2)
.succeeded(3));

events.failed()
.assertEventsMatchExactly(
event(displayClass(PinningTestCase.class), failedWithPinningException("pinningInResourceMethod")),
event(displayClass(PinningExtraThreadTestCase.class), failedWithPinningException("lambda$pinningTest$0"))
);
}

private Condition<org.junit.platform.testkit.engine.Event> failedWithPinningException(String expectedPinningMethodName) {
return finishedWithFailure(
instanceOf(PinningException.class),
message(m -> m.startsWith("Pinned virtual threads were detected"))
, new Condition<>(where(
t -> Arrays.stream(t.getStackTrace()),
s -> s
.anyMatch(e -> e.getMethodName()
.equals(expectedPinningMethodName))),
"Method with pinning is missing from stack strace.")
);
}

private Condition<Event> displayClass(Class<?> clazz) {
return displayName(Arrays.stream(clazz.getName().split("\\.")).toList().getLast());
}

@HelidonTest(pinningDetection = true)
@AddBean(PinningTestCase.TestResource.class)
static class PinningTestCase {

@Path("/pinning")
public static class TestResource {
@GET
public String pinningInResourceMethod() {
synchronized (this) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
Thread.interrupted();
}
}
return "pinning";
}
}).join();
}

@Test
void pinningTest(WebTarget target) {
target.path("/pinning")
.request()
.get(String.class);
}
}

@HelidonTest(pinningDetection = true)
static class PinningExtraThreadTestCase {

@Test
void pinningTest() throws InterruptedException {
Thread.ofVirtual().start(() -> {
synchronized (this) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
Thread.interrupted();
}
}
}).join();
}
}

@HelidonTest(pinningDetection = false)
static class PinningDisabledExtraThreadTestCase {

@Test
void pinningTest() throws InterruptedException {
Thread.ofVirtual().start(() -> {
synchronized (this) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
Thread.interrupted();
}
}
}).join();
}
}

@HelidonTest(pinningDetection = true)
static class NoPinningTestCase {

@Path("/pinning")
public static class TestResource {
@GET
public String pinningInResourceMethod() {
synchronized (this) {
try {
Thread.sleep(5);
} catch (InterruptedException e) {
Thread.interrupted();
}
}
return "NO PINNING!";
}
}

@Test
void pinningTest(WebTarget target) {
Assertions.assertEquals("NO PINNING!", target.path("/pinning")
.request()
.get(String.class));
}
}

@HelidonTest(pinningDetection = true)
static class NoPinningExtraThreadTestCase {

@Test
void pinningTest() throws InterruptedException {
Thread.ofVirtual().start(() -> {
synchronized (this) {
try {
Thread.sleep(5);
} catch (InterruptedException e) {
Thread.interrupted();
}
}
}).join();
}
}
}

0 comments on commit eb33db3

Please sign in to comment.