/*
 * Copyright (c) 2018 Red Hat, Inc.
 *
 * 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.vertx.junit5;

import io.vertx.core.Vertx;
import org.junit.jupiter.api.extension.*;
import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
import org.junit.jupiter.api.extension.ExtensionContext.Store;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Optional;
import java.util.ServiceLoader;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Supplier;
import java.util.stream.Collectors;

JUnit 5 Vert.x extension that allows parameter injection as well as an automatic lifecycle on the VertxTestContext instance.

The following types can be injected:

Author:Julien Ponge
/** * JUnit 5 Vert.x extension that allows parameter injection as well as an automatic lifecycle on the {@link VertxTestContext} instance. * <p> * The following types can be injected: * <ul> * <li>{@link Vertx}</li> * <li>{@link VertxTestContext}</li> * <li>{@link io.vertx.rxjava.core.Vertx}</li> * <li>{@link io.vertx.reactivex.core.Vertx}</li> * </ul> * * @author <a href="https://julien.ponge.org/">Julien Ponge</a> */
public final class VertxExtension implements ParameterResolver, BeforeTestExecutionCallback, AfterTestExecutionCallback, BeforeEachCallback, AfterEachCallback, BeforeAllCallback, AfterAllCallback {
Default timeout.
/** * Default timeout. */
public static final int DEFAULT_TIMEOUT_DURATION = 30;
Default timeout unit.
/** * Default timeout unit. */
public static final TimeUnit DEFAULT_TIMEOUT_UNIT = TimeUnit.SECONDS;
Key for all Vertx instances, including what shims like RxJava should use.
/** * Key for all {@link Vertx} instances, including what shims like RxJava should use. */
public static final String VERTX_INSTANCE_KEY = "Vertx"; private static final String TEST_CONTEXT_KEY = "VertxTestContext"; private static class ContextList extends ArrayList<VertxTestContext> { /* * There may be concurrent test contexts to join at a point of time because it is allowed to have several * user-defined lifecycle event handles (e.g., @BeforeEach, etc). */ } private final HashMap<Class<?>, VertxExtensionParameterProvider<?>> parameterProviders = new HashMap<>(); public VertxExtension() { for (VertxExtensionParameterProvider<?> parameterProvider : ServiceLoader.load(VertxExtensionParameterProvider.class)) { parameterProviders.put(parameterProvider.type(), parameterProvider); } } @Override public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { return parameterProviders.keySet().contains(parameterType(parameterContext)); } private Class<?> parameterType(ParameterContext parameterContext) { return parameterContext.getParameter().getType(); } @Override public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { Class<?> type = parameterType(parameterContext); VertxExtensionParameterProvider<?> parameterProvider = parameterProviders.get(type); if (type.equals(VertxTestContext.class)) { return newTestContext(extensionContext); } if (extensionContext.getParent().isPresent()) { Store parentStore = store(extensionContext.getParent().get()); if (parentStore.get(parameterProvider.key()) != null) { return unpack(parentStore.get(parameterProvider.key())); } } Store store = store(extensionContext); return unpack(store.getOrComputeIfAbsent(parameterProvider.key(), key -> new ScopedObject( parameterProvider.newInstance(extensionContext, parameterContext), parameterProvider.parameterClosingConsumer()))); } private static Object unpack(Object object) { if (object instanceof Supplier<?>) { return ((Supplier<?>) object).get(); } return object; } private VertxTestContext newTestContext(ExtensionContext extensionContext) { Store store = store(extensionContext); ContextList contexts = (ContextList) store.getOrComputeIfAbsent(TEST_CONTEXT_KEY, key -> new ContextList()); VertxTestContext newTestContext = new VertxTestContext(); contexts.add(newTestContext); return newTestContext; } private Store store(ExtensionContext extensionContext) { return extensionContext.getStore(Namespace.GLOBAL); } @Override public void beforeAll(ExtensionContext context) throws Exception { // Not much we can do here ATM } @Override public void afterAll(ExtensionContext context) throws Exception { // We may wait on test contexts from @AfterAll methods joinActiveTestContexts(context); } @Override public void beforeEach(ExtensionContext context) throws Exception { // We may wait on test contexts from @BeforeAll methods joinActiveTestContexts(context); } @Override public void afterEach(ExtensionContext context) throws Exception { // We may wait on test contexts from @AfterEach methods joinActiveTestContexts(context); } @Override public void beforeTestExecution(ExtensionContext context) throws Exception { // We may wait on test contexts from @BeforeEach methods joinActiveTestContexts(context); } @Override public void afterTestExecution(ExtensionContext context) throws Exception { // We may wait on the test context from a test joinActiveTestContexts(context); } private void joinActiveTestContexts(ExtensionContext extensionContext) throws Exception { if (extensionContext.getExecutionException().isPresent()) { return; } ContextList currentContexts = store(extensionContext).remove(TEST_CONTEXT_KEY, ContextList.class); if (currentContexts != null) { for (VertxTestContext context : currentContexts) { int timeoutDuration = DEFAULT_TIMEOUT_DURATION; TimeUnit timeoutUnit = DEFAULT_TIMEOUT_UNIT; Optional<Method> testMethod = extensionContext.getTestMethod(); if (testMethod.isPresent() && testMethod.get().isAnnotationPresent(Timeout.class)) { Timeout annotation = extensionContext.getRequiredTestMethod().getAnnotation(Timeout.class); timeoutDuration = annotation.value(); timeoutUnit = annotation.timeUnit(); } else if (extensionContext.getRequiredTestClass().isAnnotationPresent(Timeout.class)) { Timeout annotation = extensionContext.getRequiredTestClass().getAnnotation(Timeout.class); timeoutDuration = annotation.value(); timeoutUnit = annotation.timeUnit(); } if (context.awaitCompletion(timeoutDuration, timeoutUnit)) { if (context.failed()) { Throwable throwable = context.causeOfFailure(); if (throwable instanceof Exception) { throw (Exception) throwable; } else { throw new AssertionError(throwable); } } } else { String message = "The test execution timed out. Make sure your asynchronous code " + "includes calls to either VertxTestContext#completeNow(), VertxTestContext#failNow() " + "or Checkpoint#flag()"; String unsatisfiedCheckpointsDiagnosis = context.unsatisfiedCheckpointCallSites() .stream() .map(element -> "-> checkpoint in file " + element.getFileName() + " line " + element.getLineNumber()) .collect(Collectors.joining("\n")); message = message + "\n\nUnsatisfied checkpoints diagnostics:\n" + unsatisfiedCheckpointsDiagnosis; throw new TimeoutException(message); } } } if (extensionContext.getParent().isPresent()) { joinActiveTestContexts(extensionContext.getParent().get()); } } }