Skip to content

Commit

Permalink
Add tests
Browse files Browse the repository at this point in the history
Fix concurrent execution for PER_CLASS lifecycle
  • Loading branch information
romain-grecourt committed Nov 28, 2024
1 parent e3fffdd commit 35c9c34
Show file tree
Hide file tree
Showing 5 changed files with 180 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -85,15 +85,16 @@ public class HelidonJunitExtension implements BeforeEachCallback,
InvocationInterceptor,
ParameterResolver {

// TODO test @PostContruct @PreDestroy
// TODO test parallel method execution
// TODO test PER_CLASS with implicit reset

private static final Namespace NAMESPACE = Namespace.create(HelidonJunitExtension.class);

private final Map<Class<?>, ClassInfo> classInfos = new ConcurrentHashMap<>();
private final Map<Method, MethodInfo> methodInfos = new ConcurrentHashMap<>();

// TODO test [alternative, interceptor, xxx] with observer method (replicate test for OCIMetricsBean)
// TODO replicate metrics test that failed
// TODO replicate messaging that failed
// TODO replicate jan visser interceptor test failed

@Override
public Object createTestInstance(TestInstanceFactoryContext fc, ExtensionContext context) {
// Use a proxy to start the container after the test instance creation
Expand Down Expand Up @@ -145,24 +146,23 @@ public void beforeEach(ExtensionContext context) {
if (container == null || container.closed()) {
Store methodStore = context.getStore(NAMESPACE);
Lifecycle lifecycle = context.getTestInstanceLifecycle().orElse(PER_METHOD);
HelidonTestScope scope = switch (lifecycle) {
case PER_CLASS -> new HelidonTestScope.PerContainer();
case PER_METHOD -> {
scope = new HelidonTestScope.PerThread();
// put the scope in the method context store to auto-close
methodStore.put("scope", (CloseableResource) scope::close);
yield scope;
}
};
HelidonTestScope scope;
if (lifecycle == Lifecycle.PER_CLASS) {
scope = new HelidonTestScope.PerContainer();
} else {
scope = new HelidonTestScope.PerThread();
// put the scope in the method context store to auto-close
methodStore.put("scope", (CloseableResource) scope::close);
}
if (methodInfo.requiresReset()) {
// put in the method store to auto-close
container = new CloseableContainer(methodInfo, scope);
methodStore.put("container", container);
} else {
// put the "class container" in the class context store
// to re-use between methods
container = new CloseableContainer(classInfo, scope);
classStore.put("container", container);
container = classStore.getOrComputeIfAbsent("container",
k -> new CloseableContainer(classInfo, scope), CloseableContainer.class);
}
}
// proxy handler uses class context
Expand All @@ -175,23 +175,23 @@ public boolean supportsParameter(ParameterContext pc, ExtensionContext context)
throws ParameterResolutionException {

HelidonTestContainer container = container(context, context.getRequiredTestMethod());
return container.initializedFailed() || container.isSupported(pc.getParameter().getType());
return !container.initFailed() && container.isSupported(pc.getParameter().getType());
}

@Override
public Object resolveParameter(ParameterContext pc, ExtensionContext context)
throws ParameterResolutionException {

HelidonTestContainer container = container(context, context.getRequiredTestMethod());
return container.initializedFailed() ? null : container.resolveInstance(pc.getParameter().getType());
return container.initFailed() ? null : container.resolveInstance(pc.getParameter().getType());
}

private void intercept(Invocation<Void> invocation,
ReflectiveInvocationContext<Method> ic,
ExtensionContext context) throws Throwable {

HelidonTestContainer container = container(context, context.getRequiredTestMethod());
if (container.initializedFailed()) {
if (container.initFailed()) {
invocation.skip();
} else {
// proxy handler uses class context
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ boolean closed() {
*
* @return {@code true} if failed, {@code false} otherwise
*/
boolean initializedFailed() {
boolean initFailed() {
return error != null;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* 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 java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;

import io.helidon.microprofile.testing.junit5.AddBean;
import io.helidon.microprofile.testing.junit5.HelidonTest;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.parallel.Execution;
import org.junit.jupiter.api.parallel.ExecutionMode;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;

@HelidonTest
@AddBean(TestPerClassConcurrentExecution.State.class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class TestPerClassConcurrentExecution {

@Inject
State state;

@Test
@Execution(ExecutionMode.CONCURRENT)
void testA() throws InterruptedException {
state.add("a");
state.countDownA();
state.awaitB();
assertThat(state.get(), is(Set.of("a", "b")));
}

@Test
@Execution(ExecutionMode.CONCURRENT)
void testB() throws InterruptedException {
state.awaitA();
assertThat(state.get(), is(Set.of("a")));
state.add("b");
state.countDownB();
}

@ApplicationScoped
static class State {

final Set<String> events = ConcurrentHashMap.newKeySet();
final CountDownLatch latchA = new CountDownLatch(1);
final CountDownLatch latchB = new CountDownLatch(1);

void add(String event) {
events.add(event);
}

Set<String> get() {
return events;
}

void countDownA() {
latchA.countDown();
}

void awaitA() throws InterruptedException {
latchA.await();
}

void countDownB() {
latchB.countDown();
}

void awaitB() throws InterruptedException {
latchB.await();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* 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.AddBean;
import io.helidon.microprofile.testing.junit5.HelidonTest;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;

@HelidonTest
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class TestPerClassImplicitReset {

@Inject
CounterBean counterBean;

@Test
@AddBean(CounterBean.class)
void firstTest() {
assertThat(counterBean.value++, is(0));
}

@Test
@AddBean(CounterBean.class)
void secondTest() {
assertThat(counterBean.value++, is(0));
}

@ApplicationScoped
static class CounterBean {
int value = 0;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#
# 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.
#
junit.jupiter.execution.parallel.enabled=true

0 comments on commit 35c9c34

Please sign in to comment.