001/**
002 * Copyright (C) 2006-2024 Talend Inc. - www.talend.com
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.talend.sdk.component.junit5;
017
018import static java.util.Optional.ofNullable;
019
020import java.lang.annotation.Annotation;
021import java.lang.reflect.AnnotatedElement;
022import java.util.Optional;
023
024import org.junit.jupiter.api.extension.AfterAllCallback;
025import org.junit.jupiter.api.extension.AfterEachCallback;
026import org.junit.jupiter.api.extension.BeforeAllCallback;
027import org.junit.jupiter.api.extension.BeforeEachCallback;
028import org.junit.jupiter.api.extension.ExtensionContext;
029import org.junit.platform.commons.util.AnnotationUtils;
030import org.talend.sdk.component.junit.BaseComponentsHandler;
031import org.talend.sdk.component.junit.base.junit5.JUnit5InjectionSupport;
032import org.talend.sdk.component.junit.environment.Environment;
033
034/**
035 * Extension allowing the test to use a {@link org.talend.sdk.component.junit.ComponentsHandler}
036 * and auto register components from current project.
037 */
038public class ComponentExtension extends BaseComponentsHandler
039        implements BeforeAllCallback, AfterAllCallback, JUnit5InjectionSupport, BeforeEachCallback, AfterEachCallback {
040
041    public static final ExtensionContext.Namespace NAMESPACE =
042            ExtensionContext.Namespace.create(ComponentExtension.class.getName());
043
044    private static final String USE_EACH_KEY = ComponentExtension.class.getName() + ".useEach";
045
046    private static final String SHARED_INSTANCE = ComponentExtension.class.getName() + ".instance";
047
048    @Override
049    public void beforeAll(final ExtensionContext extensionContext) {
050        final WithComponents element = AnnotationUtils
051                .findAnnotation(extensionContext.getElement(), WithComponents.class)
052                .orElseThrow(() -> new IllegalArgumentException(
053                        "No annotation @WithComponents on " + extensionContext.getRequiredTestClass()));
054        this.packageName = element.value();
055        if (element.isolatedPackages().length > 0) {
056            withIsolatedPackage(null, element.isolatedPackages());
057        }
058
059        final boolean shouldUseEach = shouldIgnore(extensionContext.getElement());
060        if (!shouldUseEach) {
061            doStart(extensionContext);
062        } else if (!extensionContext.getElement().map(AnnotatedElement::getAnnotations).map(annotations -> {
063            int componentIndex = -1;
064            for (int i = 0; i < annotations.length; i++) {
065                final Class<? extends Annotation> type = annotations[i].annotationType();
066                if (type == WithComponents.class) {
067                    componentIndex = i;
068                } else if (type == Environment.class && componentIndex >= 0) {
069                    return false;
070                }
071            }
072            return true;
073        }).orElse(false)) {
074            // check the ordering, if environments are put after this then the context is likely wrong
075            // this condition is a simple heuristic but enough for most cases
076            throw new IllegalArgumentException("If you combine @WithComponents and @Environment, you must ensure "
077                    + "environment annotations are becoming before the component one otherwise you will run in an "
078                    + "unexpected context and will not reproduce real execution.");
079        }
080        extensionContext.getStore(NAMESPACE).put(USE_EACH_KEY, shouldUseEach);
081        extensionContext.getStore(NAMESPACE).put(SHARED_INSTANCE, this);
082    }
083
084    @Override
085    public void afterAll(final ExtensionContext extensionContext) {
086        if (!shouldUseEach(extensionContext)) {
087            doStop(extensionContext);
088        }
089    }
090
091    @Override
092    public Class<? extends Annotation> injectionMarker() {
093        return Injected.class;
094    }
095
096    @Override
097    public void beforeEach(final ExtensionContext extensionContext) {
098        if (!shouldUseEach(extensionContext)) {
099            doInject(extensionContext);
100        }
101    }
102
103    @Override
104    public void afterEach(final ExtensionContext extensionContext) {
105        if (!shouldUseEach(extensionContext)) {
106            resetState();
107        }
108    }
109
110    public void doStart(final ExtensionContext extensionContext) {
111        extensionContext.getStore(NAMESPACE).put(EmbeddedComponentManager.class.getName(), start());
112    }
113
114    public void doStop(final ExtensionContext extensionContext) {
115        ofNullable(EmbeddedComponentManager.class
116                .cast(extensionContext.getStore(NAMESPACE).get(EmbeddedComponentManager.class.getName())))
117                .ifPresent(EmbeddedComponentManager::close);
118    }
119
120    public void doInject(final ExtensionContext extensionContext) {
121        extensionContext.getTestInstance().ifPresent(this::injectServices);
122    }
123
124    private Boolean shouldUseEach(final ExtensionContext extensionContext) {
125        return extensionContext.getStore(NAMESPACE).get(USE_EACH_KEY, boolean.class);
126    }
127
128    private boolean shouldIgnore(final Optional<AnnotatedElement> element) {
129        return !AnnotationUtils.findRepeatableAnnotations(element, Environment.class).isEmpty();
130    }
131}