This is an automated email from the ASF dual-hosted git repository. asf-gitbox-commits pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/deltaspike.git
commit d1359dc571083cc1bfaa28e2816cfc34a360232d Author: Mark Struberg <[email protected]> AuthorDate: Fri May 8 15:59:12 2026 +0200 DELTASPIKE-1366 adding Junit5 support --- deltaspike/modules/pom.xml | 1 + deltaspike/modules/test-control5/api/pom.xml | 70 +++ .../deltaspike/testcontrol5/api/TestControl.java | 79 +++ .../testcontrol5/api/junit/CdiTestExtension.java | 628 +++++++++++++++++++++ .../api/junit/CdiTestSuiteExtension.java | 258 +++++++++ .../testcontrol5/api/junit/TestBaseConfig.java | 71 +++ .../api/literal/TestControlLiteral.java | 76 +++ .../api/mock/ApplicationMockManager.java | 28 + .../testcontrol5/api/mock/DynamicMockManager.java | 24 + .../testcontrol5/api/mock/TypedMock.java | 34 ++ .../testcontrol5/spi/ExternalContainer.java | 34 ++ .../deltaspike/testcontrol5/spi/TestAware.java | 28 + .../testcontrol5/spi/TestControlValidator.java | 27 + .../spi/junit/TestStatementDecoratorFactory.java | 28 + .../testcontrol5/spi/mock/MockFilter.java | 29 + deltaspike/modules/test-control5/impl/pom.xml | 141 +++++ .../impl/mock/AbstractMockManager.java | 112 ++++ .../testcontrol5/impl/mock/BeanCacheKey.java | 265 +++++++++ .../testcontrol5/impl/mock/DefaultMockFilter.java | 172 ++++++ .../impl/mock/MockAwareInjectionTargetWrapper.java | 99 ++++ .../impl/mock/MockAwareProducerWrapper.java | 82 +++ .../testcontrol5/impl/mock/MockExtension.java | 157 ++++++ .../impl/mock/SimpleApplicationMockManager.java | 30 + .../testcontrol5/impl/mock/SimpleMockManager.java | 47 ++ .../impl/request/ContextControlDecorator.java | 115 ++++ .../StandardContextTestControlValidator.java | 107 ++++ deltaspike/modules/{ => test-control5}/pom.xml | 21 +- 27 files changed, 2749 insertions(+), 14 deletions(-) diff --git a/deltaspike/modules/pom.xml b/deltaspike/modules/pom.xml index d4017033f..b448d066d 100644 --- a/deltaspike/modules/pom.xml +++ b/deltaspike/modules/pom.xml @@ -43,5 +43,6 @@ <module>data</module> <module>scheduler</module> <module>test-control</module> + <module>test-control5</module> </modules> </project> diff --git a/deltaspike/modules/test-control5/api/pom.xml b/deltaspike/modules/test-control5/api/pom.xml new file mode 100644 index 000000000..21ceb3e90 --- /dev/null +++ b/deltaspike/modules/test-control5/api/pom.xml @@ -0,0 +1,70 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + ~ Licensed to the Apache Software Foundation (ASF) under one + ~ or more contributor license agreements. See the NOTICE file + ~ distributed with this work for additional information + ~ regarding copyright ownership. The ASF licenses this file + ~ to you 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. + --> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <parent> + <groupId>org.apache.deltaspike.modules</groupId> + <artifactId>test-control5-module-project</artifactId> + <version>2.0.2-SNAPSHOT</version> + <relativePath>../pom.xml</relativePath> + </parent> + + <groupId>org.apache.deltaspike.modules</groupId> + <artifactId>deltaspike-test-control5-module-api</artifactId> + <packaging>jar</packaging> + + <name>Apache DeltaSpike Test-Control5-Module API (JUnit 5)</name> + + <properties> + <deltaspike.osgi.export.pkg> + org.apache.deltaspike.testcontrol5.* + </deltaspike.osgi.export.pkg> + <deltaspike.osgi.import> + jakarta.enterprise.inject, + !org.apache.deltaspike.testcontrol5.*, + * + </deltaspike.osgi.import> + </properties> + + <dependencies> + <dependency> + <groupId>org.apache.deltaspike.core</groupId> + <artifactId>deltaspike-core-api</artifactId> + <version>${project.version}</version> + </dependency> + + <dependency> + <groupId>org.apache.deltaspike.cdictrl</groupId> + <artifactId>deltaspike-cdictrl-api</artifactId> + <version>${project.version}</version> + <scope>provided</scope> + </dependency> + + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter-api</artifactId> + <version>${junit5.version}</version> + <scope>provided</scope> + </dependency> + </dependencies> + + +</project> diff --git a/deltaspike/modules/test-control5/api/src/main/java/org/apache/deltaspike/testcontrol5/api/TestControl.java b/deltaspike/modules/test-control5/api/src/main/java/org/apache/deltaspike/testcontrol5/api/TestControl.java new file mode 100644 index 000000000..816866c3f --- /dev/null +++ b/deltaspike/modules/test-control5/api/src/main/java/org/apache/deltaspike/testcontrol5/api/TestControl.java @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.deltaspike.testcontrol5.api; + +import org.apache.deltaspike.core.spi.filter.ClassFilter; +import org.apache.deltaspike.core.api.projectstage.ProjectStage; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.util.logging.ConsoleHandler; +import java.util.logging.Handler; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Optional control annotation for JUnit 5 unit-tests + */ + +@Target({ TYPE, METHOD }) +@Retention(RUNTIME) +public @interface TestControl +{ + /** + * only supports contexts supported by ContextControl#startContext + * defaults: session- and request-scope + */ + Class<? extends Annotation>[] startScopes() default { }; + + //TODO discuss callbacks + + Class<? extends ProjectStage> projectStage() default ProjectStage.UnitTest.class; + + /** + * only supported on test-class-level + */ + Class<? extends Handler> logHandler() default ConsoleHandler.class; + + /** + * Requires additional service-loader config + * Currently only supported on class-level + */ + boolean startExternalContainers() default true; + + /** + * allows to label alternative cdi-beans similar to global alternatives to bind them to 0-n tests + */ + Class<? extends Label> activeAlternativeLabel() default Label.class; + + //with cdi 1.1+ it can be used to implement labeled-alternatives without text based config + //(details see DELTASPIKE-1338) + /** + * low-level filter (mainly needed for special cases if labeled-alternatives aren't enough) + * @return the class-filter class which should be used for the current test-class + */ + Class<? extends ClassFilter> classFilter() default ClassFilter.class; + + interface Label + { + } +} diff --git a/deltaspike/modules/test-control5/api/src/main/java/org/apache/deltaspike/testcontrol5/api/junit/CdiTestExtension.java b/deltaspike/modules/test-control5/api/src/main/java/org/apache/deltaspike/testcontrol5/api/junit/CdiTestExtension.java new file mode 100644 index 000000000..49aa23ea2 --- /dev/null +++ b/deltaspike/modules/test-control5/api/src/main/java/org/apache/deltaspike/testcontrol5/api/junit/CdiTestExtension.java @@ -0,0 +1,628 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.deltaspike.testcontrol5.api.junit; + +import org.apache.deltaspike.cdise.api.CdiContainer; +import org.apache.deltaspike.cdise.api.CdiContainerLoader; +import org.apache.deltaspike.cdise.api.ContextControl; +import org.apache.deltaspike.core.api.projectstage.ProjectStage; +import org.apache.deltaspike.core.api.provider.BeanProvider; +import org.apache.deltaspike.core.util.ExceptionUtils; +import org.apache.deltaspike.core.util.ProjectStageProducer; +import org.apache.deltaspike.core.util.ServiceUtils; +import org.apache.deltaspike.testcontrol5.api.TestControl; +import org.apache.deltaspike.testcontrol5.api.literal.TestControlLiteral; +import org.apache.deltaspike.testcontrol5.spi.ExternalContainer; +import org.apache.deltaspike.testcontrol5.spi.TestAware; +import org.apache.deltaspike.testcontrol5.spi.TestControlValidator; +import org.apache.deltaspike.testcontrol5.spi.junit.TestStatementDecoratorFactory; +import org.junit.jupiter.api.extension.AfterAllCallback; +import org.junit.jupiter.api.extension.AfterEachCallback; +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.api.extension.ParameterResolutionException; +import org.junit.jupiter.api.extension.ParameterResolver; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.context.RequestScoped; +import jakarta.enterprise.context.SessionScoped; +import jakarta.inject.Singleton; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Set; +import java.util.Stack; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.Logger; + + +/** + * A JUnit 5 extension to start up with a CDI or embedded JavaEE container. + */ +public class CdiTestExtension implements BeforeAllCallback, AfterAllCallback, + BeforeEachCallback, AfterEachCallback, ParameterResolver +{ + private static final Logger LOGGER = Logger.getLogger(CdiTestExtension.class.getName()); + + private static final boolean USE_TEST_CLASS_AS_CDI_BEAN; + private static final boolean ALLOW_INJECTION_POINT_MANIPULATION; + + private static Set<Integer> extensionIdentities = new CopyOnWriteArraySet<Integer>(); + + static + { + USE_TEST_CLASS_AS_CDI_BEAN = TestBaseConfig.ContainerIntegration.USE_TEST_CLASS_AS_CDI_BEAN; + ALLOW_INJECTION_POINT_MANIPULATION = TestBaseConfig.MockIntegration.ALLOW_MANUAL_INJECTION_POINT_MANIPULATION; + } + + private static ThreadLocal<Boolean> automaticScopeHandlingActive = new ThreadLocal<Boolean>(); + + private static ThreadLocal<CdiTestExtension> currentTestExtension = new ThreadLocal<CdiTestExtension>(); + + private List<TestStatementDecoratorFactory> statementDecoratorFactories; + + private ContainerAwareTestContext testContext; + + public CdiTestExtension() + { + } + + @Override + public void beforeEach(ExtensionContext extensionContext) throws Exception + { + currentTestExtension.set(this); + + TestControl testControl = extensionContext.getTestClass() + .map(cls -> cls.getAnnotation(TestControl.class)).orElse(null); + + ContainerAwareTestContext currentTestContext = + new ContainerAwareTestContext(testControl, this.testContext); + + extensionContext.getTestMethod().ifPresent(method -> + { + try + { + currentTestContext.applyBeforeMethodConfig(method); + } + catch (Exception e) + { + throw ExceptionUtils.throwAsRuntimeException(e); + } + }); + + this.testContext = currentTestContext; + } + + @Override + public void afterEach(ExtensionContext extensionContext) throws Exception + { + try + { + if (this.testContext != null) + { + this.testContext.applyAfterMethodConfig(); + } + } + finally + { + currentTestExtension.set(null); + currentTestExtension.remove(); + } + } + + @Override + public void beforeAll(ExtensionContext extensionContext) throws Exception + { + if (this.testContext == null) + { + TestControl testControl = extensionContext.getTestClass() + .map(cls -> cls.getAnnotation(TestControl.class)).orElse(null); + + this.testContext = new ContainerAwareTestContext(testControl, null); + + Class<? extends Handler> logHandlerClass = this.testContext.getLogHandlerClass(); + + if (!Handler.class.equals(logHandlerClass)) + { + try + { + LOGGER.addHandler(logHandlerClass.newInstance()); + } + catch (Exception e) + { + throw ExceptionUtils.throwAsRuntimeException(e); + } + } + + this.statementDecoratorFactories = ServiceUtils.loadServiceImplementations(TestStatementDecoratorFactory.class); + Collections.sort(this.statementDecoratorFactories, new Comparator<TestStatementDecoratorFactory>() + { + @Override + public int compare(TestStatementDecoratorFactory f1, TestStatementDecoratorFactory f2) + { + return f1.getOrdinal() > f2.getOrdinal() ? 1 : -1; + } + }); + } + + this.testContext.applyBeforeClassConfig(extensionContext.getTestClass().orElseThrow()); + } + + @Override + public void afterAll(ExtensionContext extensionContext) throws Exception + { + if (this.testContext != null) + { + this.testContext.applyAfterClassConfig(); + } + } + + @Override + public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException + { + return false; + } + + @Override + public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException + { + return null; + } + + private class ContainerAwareTestContext + { + private ContainerAwareTestContext parent; + + private final ProjectStage projectStage; + private final TestControl testControl; + + private ProjectStage previousProjectStage; + + private boolean containerStarted = false; + + private Stack<Class<? extends Annotation>> startedScopes = new Stack<Class<? extends Annotation>>(); + + private List<ExternalContainer> externalContainers; + + ContainerAwareTestContext(TestControl testControl, ContainerAwareTestContext parent) + { + this.parent = parent; + + Class<? extends ProjectStage> foundProjectStageClass; + if (testControl == null) + { + this.testControl = new TestControlLiteral(); + if (parent != null) + { + foundProjectStageClass = parent.testControl.projectStage(); + } + else + { + foundProjectStageClass = this.testControl.projectStage(); + } + } + else + { + this.testControl = testControl; + foundProjectStageClass = this.testControl.projectStage(); + } + this.projectStage = ProjectStage.valueOf(foundProjectStageClass.getSimpleName()); + + ProjectStageProducer.setProjectStage(this.projectStage); + } + + boolean isContainerStarted() + { + return this.containerStarted || (this.parent != null && this.parent.isContainerStarted()); + } + + Class<? extends Handler> getLogHandlerClass() + { + return this.testControl.logHandler(); + } + + void applyBeforeClassConfig(Class<?> testClass) + { + CdiContainer container = CdiContainerLoader.getCdiContainer(); + + if (!isContainerStarted()) + { + System.setProperty("org.jboss.weld.environment.servlet.archive.isolation", "false"); + + container.boot(CdiTestSuiteExtension.getTestContainerConfig()); + setContainerStarted(); + + bootExternalContainers(testClass); + } + + List<Class<? extends Annotation>> restrictedScopes = new ArrayList<Class<? extends Annotation>>(); + + restrictedScopes.add(ApplicationScoped.class); + restrictedScopes.add(Singleton.class); + + if (this.parent == null && this.testControl.getClass().equals(TestControlLiteral.class)) + { + restrictedScopes.add(RequestScoped.class); + restrictedScopes.add(SessionScoped.class); + } + + startScopes(container, testClass, null, restrictedScopes.toArray(new Class[restrictedScopes.size()])); + } + + private void bootExternalContainers(Class testClass) + { + if (!this.testControl.startExternalContainers()) + { + return; + } + + if (this.externalContainers == null) + { + List<ExternalContainer> configuredExternalContainers = + ServiceUtils.loadServiceImplementations(ExternalContainer.class); + Collections.sort(configuredExternalContainers, new Comparator<ExternalContainer>() + { + @Override + public int compare(ExternalContainer ec1, ExternalContainer ec2) + { + return ec1.getOrdinal() > ec2.getOrdinal() ? 1 : -1; + } + }); + + this.externalContainers = new ArrayList<ExternalContainer>(configuredExternalContainers.size()); + + ExternalContainer externalContainerBean; + for (ExternalContainer externalContainer : configuredExternalContainers) + { + externalContainerBean = BeanProvider.getContextualReference(externalContainer.getClass(), true); + + if (externalContainerBean != null) + { + this.externalContainers.add(externalContainerBean); + } + else + { + this.externalContainers.add(externalContainer); + } + } + + for (ExternalContainer externalContainer : this.externalContainers) + { + try + { + if (externalContainer instanceof TestAware) + { + ((TestAware) externalContainer).setTestClass(testClass); + } + externalContainer.boot(); + } + catch (RuntimeException e) + { + Logger.getLogger(CdiTestExtension.class.getName()).log(Level.WARNING, + "booting " + externalContainer.getClass().getName() + " failed", e); + } + } + } + } + + void applyAfterClassConfig() + { + CdiContainer container = CdiContainerLoader.getCdiContainer(); + + stopStartedScopes(container); + + if (this.containerStarted) + { + if (CdiTestSuiteExtension.isStopContainerAllowed()) + { + shutdownExternalContainers(); + + container.shutdown(); + CdiTestSuiteExtension.setContainerStarted(false); + } + } + } + + private void shutdownExternalContainers() + { + if (this.externalContainers == null) + { + return; + } + + for (ExternalContainer externalContainer : this.externalContainers) + { + try + { + externalContainer.shutdown(); + } + catch (RuntimeException e) + { + Logger.getLogger(CdiTestExtension.class.getName()).log(Level.WARNING, + "shutting down " + externalContainer.getClass().getName() + " failed", e); + } + } + } + + void applyBeforeMethodConfig(Method testMethod) + { + this.previousProjectStage = ProjectStageProducer.getInstance().getProjectStage(); + ProjectStageProducer.setProjectStage(this.projectStage); + + setCurrentTestMethod(testMethod); + startScopes(CdiContainerLoader.getCdiContainer(), testMethod.getDeclaringClass(), testMethod); + } + + void applyAfterMethodConfig() + { + try + { + stopStartedScopes(CdiContainerLoader.getCdiContainer()); + } + finally + { + setCurrentTestMethod(null); + ProjectStageProducer.setProjectStage(previousProjectStage); + previousProjectStage = null; + } + } + + void setContainerStarted() + { + this.containerStarted = true; + CdiTestSuiteExtension.setContainerStarted(true); + } + + private void startScopes(CdiContainer container, + Class testClass, + Method testMethod, + Class<? extends Annotation>... restrictedScopes) + { + try + { + automaticScopeHandlingActive.set(true); + + ContextControl contextControl = container.getContextControl(); + + List<Class<? extends Annotation>> scopeClasses = new ArrayList<Class<? extends Annotation>>(); + + Collections.addAll(scopeClasses, this.testControl.startScopes()); + + if (scopeClasses.isEmpty()) + { + addScopesForDefaultBehavior(scopeClasses); + } + else + { + List<TestControlValidator> testControlValidatorList = + ServiceUtils.loadServiceImplementations(TestControlValidator.class); + + for (TestControlValidator testControlValidator : testControlValidatorList) + { + if (testControlValidator instanceof TestAware) + { + if (testMethod != null) + { + ((TestAware) testControlValidator).setTestMethod(testMethod); + } + ((TestAware) testControlValidator).setTestClass(testClass); + } + try + { + testControlValidator.validate(this.testControl); + } + finally + { + if (testControlValidator instanceof TestAware) + { + ((TestAware) testControlValidator).setTestClass(null); + ((TestAware) testControlValidator).setTestMethod(null); + } + } + } + } + + for (Class<? extends Annotation> scopeAnnotation : scopeClasses) + { + if (this.parent != null && this.parent.isScopeStarted(scopeAnnotation)) + { + continue; + } + + if (isRestrictedScope(scopeAnnotation, restrictedScopes)) + { + continue; + } + + try + { + contextControl.stopContext(scopeAnnotation); + + contextControl.startContext(scopeAnnotation); + this.startedScopes.add(scopeAnnotation); + + onScopeStarted(scopeAnnotation); + } + catch (RuntimeException e) + { + Logger logger = Logger.getLogger(CdiTestExtension.class.getName()); + logger.setLevel(Level.SEVERE); + logger.log(Level.SEVERE, "failed to start scope @" + scopeAnnotation.getName(), e); + } + } + } + finally + { + automaticScopeHandlingActive.set(null); + automaticScopeHandlingActive.remove(); + } + } + + private void addScopesForDefaultBehavior(List<Class<? extends Annotation>> scopeClasses) + { + if (this.parent != null && !this.parent.isScopeStarted(RequestScoped.class)) + { + if (!scopeClasses.contains(RequestScoped.class)) + { + scopeClasses.add(RequestScoped.class); + } + } + if (this.parent != null && !this.parent.isScopeStarted(SessionScoped.class)) + { + if (!scopeClasses.contains(SessionScoped.class)) + { + scopeClasses.add(SessionScoped.class); + } + } + } + + private boolean isRestrictedScope(Class<? extends Annotation> scopeAnnotation, + Class<? extends Annotation>[] restrictedScopes) + { + for (Class<? extends Annotation> restrictedScope : restrictedScopes) + { + if (scopeAnnotation.equals(restrictedScope)) + { + return true; + } + } + return false; + } + + private boolean isScopeStarted(Class<? extends Annotation> scopeAnnotation) + { + return this.startedScopes.contains(scopeAnnotation); + } + + private void stopStartedScopes(CdiContainer container) + { + try + { + automaticScopeHandlingActive.set(true); + + while (!this.startedScopes.empty()) + { + Class<? extends Annotation> scopeAnnotation = this.startedScopes.pop(); + try + { + container.getContextControl().stopContext(scopeAnnotation); + onScopeStopped(scopeAnnotation); + } + catch (RuntimeException e) + { + Logger logger = Logger.getLogger(CdiTestExtension.class.getName()); + logger.setLevel(Level.SEVERE); + logger.log(Level.SEVERE, "failed to stop scope @" + scopeAnnotation.getName(), e); + } + } + } + finally + { + automaticScopeHandlingActive.remove(); + automaticScopeHandlingActive.set(null); + } + } + + private void onScopeStarted(Class<? extends Annotation> scopeClass) + { + List<ExternalContainer> externalContainerList = collectExternalContainers(this); + + for (ExternalContainer externalContainer : externalContainerList) + { + externalContainer.startScope(scopeClass); + } + } + + private void onScopeStopped(Class<? extends Annotation> scopeClass) + { + List<ExternalContainer> externalContainerList = collectExternalContainers(this); + + for (ExternalContainer externalContainer : externalContainerList) + { + externalContainer.stopScope(scopeClass); + } + } + + private List<ExternalContainer> collectExternalContainers(ContainerAwareTestContext testContext) + { + List<ExternalContainer> result = new ArrayList<ExternalContainer>(); + + if (testContext.externalContainers != null) + { + result.addAll(testContext.externalContainers); + } + + if (testContext.parent != null) + { + result.addAll(collectExternalContainers(testContext.parent)); + } + return result; + } + + private void setCurrentTestMethod(Method testMethod) + { + List<ExternalContainer> externalContainerList = collectExternalContainers(this); + + for (ExternalContainer externalContainer : externalContainerList) + { + if (externalContainer instanceof TestAware) + { + try + { + ((TestAware) externalContainer).setTestMethod(testMethod); + } + catch (Throwable t) + { + throw new RuntimeException(t.getMessage()); + } + } + } + } + } + + public static Boolean isAutomaticScopeHandlingActive() + { + return automaticScopeHandlingActive.get(); + } + + public static List<ExternalContainer> getActiveExternalContainers() + { + CdiTestExtension cdiTestExtension = currentTestExtension.get(); + + if (cdiTestExtension == null || + cdiTestExtension.testContext == null || + cdiTestExtension.testContext.externalContainers == null) + { + return Collections.emptyList(); + } + + return Collections.unmodifiableList(cdiTestExtension.testContext.externalContainers); + } + +} diff --git a/deltaspike/modules/test-control5/api/src/main/java/org/apache/deltaspike/testcontrol5/api/junit/CdiTestSuiteExtension.java b/deltaspike/modules/test-control5/api/src/main/java/org/apache/deltaspike/testcontrol5/api/junit/CdiTestSuiteExtension.java new file mode 100644 index 000000000..18b305820 --- /dev/null +++ b/deltaspike/modules/test-control5/api/src/main/java/org/apache/deltaspike/testcontrol5/api/junit/CdiTestSuiteExtension.java @@ -0,0 +1,258 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.deltaspike.testcontrol5.api.junit; + +import org.apache.deltaspike.cdise.api.CdiContainer; +import org.apache.deltaspike.cdise.api.CdiContainerLoader; +import org.apache.deltaspike.core.api.config.ConfigResolver; +import org.apache.deltaspike.core.api.config.PropertyLoader; +import org.apache.deltaspike.core.spi.filter.ClassFilter; +import org.apache.deltaspike.core.spi.activation.Deactivatable; +import org.apache.deltaspike.core.spi.config.ConfigSource; +import org.apache.deltaspike.core.util.ClassDeactivationUtils; +import org.apache.deltaspike.testcontrol5.api.TestControl; +import org.junit.jupiter.api.extension.AfterAllCallback; +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.ExtensionContext; + +import jakarta.inject.Named; + +import java.util.Arrays; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.ConcurrentHashMap; + + +public class CdiTestSuiteExtension implements BeforeAllCallback, AfterAllCallback +{ + public static final String CUSTOM_TEST_CONTAINER_CONFIG_FILE_KEY = + "deltaspike.testcontrol5.test-container.config-file"; + + public static final String DEFAULT_TEST_CONTAINER_CONFIG_FILE_NAME = + "META-INF/apache-deltaspike_test-container"; + + private static final boolean STOP_CONTAINER; + + private static final ThreadLocal<Boolean> IS_CDI_TEST_RUNNER_EXECUTION = new ThreadLocal<Boolean>(); + + private static volatile boolean containerStarted; + + private Class<?> testSuiteClass; + + static + { + STOP_CONTAINER = TestBaseConfig.ContainerIntegration.STOP_CONTAINER; + } + + public CdiTestSuiteExtension() + { + } + + @Override + public void beforeAll(ExtensionContext extensionContext) throws Exception + { + this.testSuiteClass = extensionContext.getTestClass() + .orElseThrow(() -> new IllegalStateException("no test-suite class found")); + + CdiContainer container = CdiContainerLoader.getCdiContainer(); + + if (!containerStarted) + { + applyTestSpecificMetaData(testSuiteClass); + + container.boot(getTestContainerConfig()); + containerStarted = true; + } + + try + { + // Tests will be executed by JUnit 5 + } + finally + { + if (STOP_CONTAINER) + { + container.shutdown(); + containerStarted = false; + } + } + } + + @Override + public void afterAll(ExtensionContext extensionContext) throws Exception + { + // Cleanup if needed + } + + public static boolean isContainerStarted() + { + return containerStarted; + } + + static Boolean isStopContainerAllowed() + { + return STOP_CONTAINER; + } + + static ThreadLocal<Boolean> getCdiTestRunnerExecutionRef() + { + return IS_CDI_TEST_RUNNER_EXECUTION; + } + + static void setContainerStarted(boolean containerStarted) + { + CdiTestSuiteExtension.containerStarted = containerStarted; + } + + public static Properties getTestContainerConfig() + { + String cdiTestRunnerConfig = ConfigResolver.getProjectStageAwarePropertyValue( + CUSTOM_TEST_CONTAINER_CONFIG_FILE_KEY, DEFAULT_TEST_CONTAINER_CONFIG_FILE_NAME); + return PropertyLoader.getProperties(cdiTestRunnerConfig); + } + + static void applyTestSpecificMetaData(Class<?> currentAnnotationSource) + { + TestControl testControl = currentAnnotationSource.getAnnotation(TestControl.class); + String activeAlternativeLabel = checkForLabeledAlternativeConfig(testControl); + + initTestEnvConfig(currentAnnotationSource, activeAlternativeLabel, testControl); + } + + private static String checkForLabeledAlternativeConfig(TestControl testControl) + { + String activeAlternativeLabel = ""; + + if (testControl != null) + { + Class<? extends TestControl.Label> activeTypedAlternativeLabel = + testControl.activeAlternativeLabel(); + + if (!TestControl.Label.class.equals(activeTypedAlternativeLabel)) + { + Named labelName = activeTypedAlternativeLabel.getAnnotation(Named.class); + + if (labelName != null) + { + activeAlternativeLabel = labelName.value(); + } + else + { + String labelClassName = activeTypedAlternativeLabel.getSimpleName(); + activeAlternativeLabel = labelClassName.substring(0, 1).toLowerCase(); + + if (labelClassName.length() > 1) + { + activeAlternativeLabel += labelClassName.substring(1); + } + } + } + } + return activeAlternativeLabel; + } + + private static void initTestEnvConfig(Class<?> testClass, String activeAlternativeLabel, TestControl testControl) + { + if (ClassDeactivationUtils.isActivated(TestConfigSource.class)) + { + TestConfigSource testConfigSource = null; + + for (ConfigSource configSource : ConfigResolver.getConfigSources()) + { + if (configSource instanceof TestConfigSource) + { + testConfigSource = (TestConfigSource) configSource; + } + } + + if (testConfigSource == null) + { + testConfigSource = new TestConfigSource(); + ConfigResolver.addConfigSources(Arrays.<ConfigSource>asList(testConfigSource)); + } + + testConfigSource.getProperties().put("activeAlternativeLabel", activeAlternativeLabel); + + testConfigSource.getProperties().put("activeAlternativeLabelSource", testClass.getName()); + + if (testControl != null) + { + testConfigSource.getProperties().put(TestControl.class.getName(), testClass.getName()); + testConfigSource.getProperties().put(ClassFilter.class.getName(), testControl.classFilter().getName()); + } + else + { + testConfigSource.getProperties().put(TestControl.class.getName(), TestControl.class.getName()); + testConfigSource.getProperties().put(ClassFilter.class.getName(), ClassFilter.class.getName()); + } + } + else + { + System.setProperty("activeAlternativeLabel", activeAlternativeLabel); + + System.setProperty("activeAlternativeLabelSource", testClass.getName()); + + if (testControl != null) + { + System.setProperty(TestControl.class.getName(), testClass.getName()); + System.setProperty(ClassFilter.class.getName(), testControl.classFilter().getName()); + } + else + { + System.setProperty(TestControl.class.getName(), TestControl.class.getName()); + System.setProperty(ClassFilter.class.getName(), ClassFilter.class.getName()); + } + } + } + + public static class TestConfigSource implements ConfigSource, Deactivatable + { + private Map<String, String> testConfig = new ConcurrentHashMap<String, String>(); + + @Override + public int getOrdinal() + { + return Integer.MIN_VALUE; + } + + @Override + public Map<String, String> getProperties() + { + return testConfig; + } + + @Override + public String getPropertyValue(String key) + { + return testConfig.get(key); + } + + @Override + public String getConfigName() + { + return "ds-test5-config"; + } + + @Override + public boolean isScannable() + { + return true; + } + } +} diff --git a/deltaspike/modules/test-control5/api/src/main/java/org/apache/deltaspike/testcontrol5/api/junit/TestBaseConfig.java b/deltaspike/modules/test-control5/api/src/main/java/org/apache/deltaspike/testcontrol5/api/junit/TestBaseConfig.java new file mode 100644 index 000000000..0928e3fae --- /dev/null +++ b/deltaspike/modules/test-control5/api/src/main/java/org/apache/deltaspike/testcontrol5/api/junit/TestBaseConfig.java @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.deltaspike.testcontrol5.api.junit; + +import org.apache.deltaspike.core.api.config.ConfigResolver; +import org.apache.deltaspike.core.api.config.base.DeltaSpikeBaseConfig; + +public interface TestBaseConfig extends DeltaSpikeBaseConfig +{ + interface ContainerIntegration + { + //default is false to improve the compatibility with @Before and @After + Boolean USE_TEST_CLASS_AS_CDI_BEAN = ConfigResolver.resolve("deltaspike.testcontrol5.use_test_class_as_cdi_bean") + .as(Boolean.class) + .withCurrentProjectStage(true) + .withDefault(Boolean.FALSE) + .getValue(); + + Boolean STOP_CONTAINER = ConfigResolver.resolve("deltaspike.testcontrol5.stop_container") + .as(Boolean.class) + .withCurrentProjectStage(true) + .withDefault(Boolean.TRUE) + .getValue(); + } + + interface MockIntegration + { + String ALLOW_MOCKED_BEANS_KEY = "deltaspike.testcontrol5.mock-support.allow_mocked_beans"; + String ALLOW_MOCKED_PRODUCERS_KEY = "deltaspike.testcontrol5.mock-support.allow_mocked_producers"; + String ALLOW_MANUAL_INJECTION_POINT_MANIPULATION_KEY = + "deltaspike.testcontrol5.mock-support.allow_manual_injection-point_manipulation"; + + Boolean ALLOW_MOCKED_BEANS = ConfigResolver.resolve(ALLOW_MOCKED_BEANS_KEY) + .as(Boolean.class) + .withCurrentProjectStage(true) + .withDefault(Boolean.FALSE) + .getValue(); + + Boolean ALLOW_MOCKED_PRODUCERS = ConfigResolver.resolve(ALLOW_MOCKED_PRODUCERS_KEY) + .as(Boolean.class) + .withCurrentProjectStage(true) + .withDefault(Boolean.FALSE) + .getValue(); + + //if enabled it's possible to change the value of injection-points after the injection-process and + //before test-execution. that allows to replace injection-points (e.g. with a mock) conditionally + //via a test-rule or @Before + Boolean ALLOW_MANUAL_INJECTION_POINT_MANIPULATION = + ConfigResolver.resolve(ALLOW_MANUAL_INJECTION_POINT_MANIPULATION_KEY) + .as(Boolean.class) + .withCurrentProjectStage(true) + .withDefault(Boolean.FALSE) + .getValue(); + } +} diff --git a/deltaspike/modules/test-control5/api/src/main/java/org/apache/deltaspike/testcontrol5/api/literal/TestControlLiteral.java b/deltaspike/modules/test-control5/api/src/main/java/org/apache/deltaspike/testcontrol5/api/literal/TestControlLiteral.java new file mode 100644 index 000000000..a5edd44e2 --- /dev/null +++ b/deltaspike/modules/test-control5/api/src/main/java/org/apache/deltaspike/testcontrol5/api/literal/TestControlLiteral.java @@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.deltaspike.testcontrol5.api.literal; + +import org.apache.deltaspike.core.spi.filter.ClassFilter; +import org.apache.deltaspike.core.api.projectstage.ProjectStage; +import org.apache.deltaspike.core.util.metadata.AnnotationInstanceProvider; +import org.apache.deltaspike.testcontrol5.api.TestControl; + +import jakarta.enterprise.util.AnnotationLiteral; +import java.lang.annotation.Annotation; +import java.util.logging.Handler; + +public class TestControlLiteral extends AnnotationLiteral<TestControl> implements TestControl +{ + private static final long serialVersionUID = 6684011035751678259L; + + private final TestControl defaultInstance; + + public TestControlLiteral() + { + this.defaultInstance = AnnotationInstanceProvider.of(TestControl.class); + } + + @Override + public Class<? extends Annotation>[] startScopes() + { + return defaultInstance.startScopes(); + } + + @Override + public Class<? extends ProjectStage> projectStage() + { + return defaultInstance.projectStage(); + } + + @Override + public Class<? extends Handler> logHandler() + { + return defaultInstance.logHandler(); + } + + @Override + public boolean startExternalContainers() + { + return defaultInstance.startExternalContainers(); + } + + @Override + public Class<? extends Label> activeAlternativeLabel() + { + return defaultInstance.activeAlternativeLabel(); + } + + @Override + public Class<? extends ClassFilter> classFilter() + { + return defaultInstance.classFilter(); + } +} diff --git a/deltaspike/modules/test-control5/api/src/main/java/org/apache/deltaspike/testcontrol5/api/mock/ApplicationMockManager.java b/deltaspike/modules/test-control5/api/src/main/java/org/apache/deltaspike/testcontrol5/api/mock/ApplicationMockManager.java new file mode 100644 index 000000000..7dff1f36d --- /dev/null +++ b/deltaspike/modules/test-control5/api/src/main/java/org/apache/deltaspike/testcontrol5/api/mock/ApplicationMockManager.java @@ -0,0 +1,28 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.deltaspike.testcontrol5.api.mock; + +import java.lang.annotation.Annotation; + +public interface ApplicationMockManager +{ + void addMock(Object mockInstance, Annotation... qualifiers); + + <T> T getMock(Class<T> beanClass, Annotation... qualifiers); +} diff --git a/deltaspike/modules/test-control5/api/src/main/java/org/apache/deltaspike/testcontrol5/api/mock/DynamicMockManager.java b/deltaspike/modules/test-control5/api/src/main/java/org/apache/deltaspike/testcontrol5/api/mock/DynamicMockManager.java new file mode 100644 index 000000000..c41178eb8 --- /dev/null +++ b/deltaspike/modules/test-control5/api/src/main/java/org/apache/deltaspike/testcontrol5/api/mock/DynamicMockManager.java @@ -0,0 +1,24 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.deltaspike.testcontrol5.api.mock; + +public interface DynamicMockManager extends ApplicationMockManager +{ + void reset(); +} diff --git a/deltaspike/modules/test-control5/api/src/main/java/org/apache/deltaspike/testcontrol5/api/mock/TypedMock.java b/deltaspike/modules/test-control5/api/src/main/java/org/apache/deltaspike/testcontrol5/api/mock/TypedMock.java new file mode 100644 index 000000000..ff06c76f6 --- /dev/null +++ b/deltaspike/modules/test-control5/api/src/main/java/org/apache/deltaspike/testcontrol5/api/mock/TypedMock.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.deltaspike.testcontrol5.api.mock; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Target({ FIELD, METHOD, TYPE }) +@Retention(RUNTIME) +public @interface TypedMock +{ + Class<?>[] value() default { }; +} diff --git a/deltaspike/modules/test-control5/api/src/main/java/org/apache/deltaspike/testcontrol5/spi/ExternalContainer.java b/deltaspike/modules/test-control5/api/src/main/java/org/apache/deltaspike/testcontrol5/spi/ExternalContainer.java new file mode 100644 index 000000000..189482b14 --- /dev/null +++ b/deltaspike/modules/test-control5/api/src/main/java/org/apache/deltaspike/testcontrol5/spi/ExternalContainer.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.deltaspike.testcontrol5.spi; + +import java.lang.annotation.Annotation; + +public interface ExternalContainer +{ + void boot(); + + void shutdown(); + + int getOrdinal(); + + void startScope(Class<? extends Annotation> scopeClass); + + void stopScope(Class<? extends Annotation> scopeClass); +} diff --git a/deltaspike/modules/test-control5/api/src/main/java/org/apache/deltaspike/testcontrol5/spi/TestAware.java b/deltaspike/modules/test-control5/api/src/main/java/org/apache/deltaspike/testcontrol5/spi/TestAware.java new file mode 100644 index 000000000..19de3b912 --- /dev/null +++ b/deltaspike/modules/test-control5/api/src/main/java/org/apache/deltaspike/testcontrol5/spi/TestAware.java @@ -0,0 +1,28 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.deltaspike.testcontrol5.spi; + +import java.lang.reflect.Method; + +public interface TestAware +{ + void setTestClass(Class testClass); + + void setTestMethod(Method testMethod); +} diff --git a/deltaspike/modules/test-control5/api/src/main/java/org/apache/deltaspike/testcontrol5/spi/TestControlValidator.java b/deltaspike/modules/test-control5/api/src/main/java/org/apache/deltaspike/testcontrol5/spi/TestControlValidator.java new file mode 100644 index 000000000..0475047a5 --- /dev/null +++ b/deltaspike/modules/test-control5/api/src/main/java/org/apache/deltaspike/testcontrol5/spi/TestControlValidator.java @@ -0,0 +1,27 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.deltaspike.testcontrol5.spi; + +import org.apache.deltaspike.core.spi.activation.Deactivatable; +import org.apache.deltaspike.testcontrol5.api.TestControl; + +public interface TestControlValidator extends Deactivatable +{ + void validate(TestControl testControl); +} diff --git a/deltaspike/modules/test-control5/api/src/main/java/org/apache/deltaspike/testcontrol5/spi/junit/TestStatementDecoratorFactory.java b/deltaspike/modules/test-control5/api/src/main/java/org/apache/deltaspike/testcontrol5/spi/junit/TestStatementDecoratorFactory.java new file mode 100644 index 000000000..ba68a53a3 --- /dev/null +++ b/deltaspike/modules/test-control5/api/src/main/java/org/apache/deltaspike/testcontrol5/spi/junit/TestStatementDecoratorFactory.java @@ -0,0 +1,28 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.deltaspike.testcontrol5.spi.junit; + +import org.junit.jupiter.api.extension.ExtensionContext; + +public interface TestStatementDecoratorFactory +{ + ExtensionContext createContext(ExtensionContext extensionContext); + + int getOrdinal(); +} diff --git a/deltaspike/modules/test-control5/api/src/main/java/org/apache/deltaspike/testcontrol5/spi/mock/MockFilter.java b/deltaspike/modules/test-control5/api/src/main/java/org/apache/deltaspike/testcontrol5/spi/mock/MockFilter.java new file mode 100644 index 000000000..5b804f093 --- /dev/null +++ b/deltaspike/modules/test-control5/api/src/main/java/org/apache/deltaspike/testcontrol5/spi/mock/MockFilter.java @@ -0,0 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.deltaspike.testcontrol5.spi.mock; + +import org.apache.deltaspike.core.spi.activation.Deactivatable; + +import jakarta.enterprise.inject.spi.Annotated; +import jakarta.enterprise.inject.spi.BeanManager; + +public interface MockFilter extends Deactivatable +{ + boolean isMockedImplementationSupported(BeanManager beanManager, Annotated annotated); +} diff --git a/deltaspike/modules/test-control5/impl/pom.xml b/deltaspike/modules/test-control5/impl/pom.xml new file mode 100644 index 000000000..a9cbed09c --- /dev/null +++ b/deltaspike/modules/test-control5/impl/pom.xml @@ -0,0 +1,141 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + ~ Licensed to the Apache Software Foundation (ASF) under one + ~ or more contributor license agreements. See the NOTICE file + ~ distributed with this work for additional information + ~ regarding copyright ownership. The ASF licenses this file + ~ to you 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. + --> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <parent> + <groupId>org.apache.deltaspike.modules</groupId> + <artifactId>test-control5-module-project</artifactId> + <version>2.0.2-SNAPSHOT</version> + <relativePath>../pom.xml</relativePath> + </parent> + + <groupId>org.apache.deltaspike.modules</groupId> + <artifactId>deltaspike-test-control5-module-impl</artifactId> + <packaging>jar</packaging> + + <name>Apache DeltaSpike Test-Control5-Module Impl (JUnit 5)</name> + + <properties> + <deltaspike.osgi.export.pkg> + org.apache.deltaspike.testcontrol5.impl.* + </deltaspike.osgi.export.pkg> + <deltaspike.osgi.import> + !org.apache.deltaspike.testcontrol5.impl.*, + * + </deltaspike.osgi.import> + </properties> + + <dependencies> + <dependency> + <groupId>org.apache.deltaspike.modules</groupId> + <artifactId>deltaspike-test-control5-module-api</artifactId> + <version>${project.version}</version> + </dependency> + + <dependency> + <groupId>org.apache.deltaspike.cdictrl</groupId> + <artifactId>deltaspike-cdictrl-api</artifactId> + <version>${project.version}</version> + <scope>provided</scope> + </dependency> + + <dependency> + <groupId>org.apache.geronimo.specs</groupId> + <artifactId>geronimo-jpa_2.2_spec</artifactId> + <optional>true</optional> + </dependency> + + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter-api</artifactId> + <version>${junit5.version}</version> + <scope>provided</scope> + </dependency> + + <dependency> + <groupId>jakarta.el</groupId> + <artifactId>jakarta.el-api</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.apache.tomcat</groupId> + <artifactId>tomcat-servlet-api</artifactId> + <scope>provided</scope> + </dependency> + + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-core</artifactId> + </dependency> + + <dependency> + <groupId>org.apache.deltaspike.core</groupId> + <artifactId>deltaspike-core-impl</artifactId> + <version>${project.version}</version> + <scope>runtime</scope> + </dependency> + </dependencies> + + + <profiles> + <profile> + <id>OWB</id> + <activation> + <activeByDefault>true</activeByDefault> + </activation> + <dependencies> + <dependency> + <groupId>org.apache.openwebbeans</groupId> + <artifactId>openwebbeans-impl</artifactId> + <scope>test</scope> + </dependency> + + <dependency> + <groupId>org.apache.openwebbeans</groupId> + <artifactId>openwebbeans-spi</artifactId> + <scope>test</scope> + </dependency> + + <dependency> + <groupId>org.apache.deltaspike.cdictrl</groupId> + <artifactId>deltaspike-cdictrl-owb</artifactId> + <version>${project.version}</version> + <scope>test</scope> + </dependency> + </dependencies> + </profile> + <profile> + <id>Weld</id> + <dependencies> + <!-- Adding a test dependency needed for Weld --> + <dependency> + <groupId>org.apache.deltaspike.cdictrl</groupId> + <artifactId>deltaspike-cdictrl-weld</artifactId> + <version>${project.version}</version> + <scope>test</scope> + </dependency> + + <!-- The remaining bits of this profile are located under deltaspike/parent/code/pom.xml --> + </dependencies> + </profile> + </profiles> + +</project> diff --git a/deltaspike/modules/test-control5/impl/src/main/java/org/apache/deltaspike/testcontrol5/impl/mock/AbstractMockManager.java b/deltaspike/modules/test-control5/impl/src/main/java/org/apache/deltaspike/testcontrol5/impl/mock/AbstractMockManager.java new file mode 100644 index 000000000..d486d27b5 --- /dev/null +++ b/deltaspike/modules/test-control5/impl/src/main/java/org/apache/deltaspike/testcontrol5/impl/mock/AbstractMockManager.java @@ -0,0 +1,112 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.deltaspike.testcontrol5.impl.mock; + +import jakarta.enterprise.inject.Typed; +import org.apache.deltaspike.testcontrol5.api.junit.TestBaseConfig; +import org.apache.deltaspike.testcontrol5.api.mock.DynamicMockManager; +import org.apache.deltaspike.testcontrol5.api.mock.TypedMock; + +import java.lang.annotation.Annotation; +import java.util.HashMap; +import java.util.Map; + +public abstract class AbstractMockManager implements DynamicMockManager +{ + private Map<BeanCacheKey, Object> registeredMocks = new HashMap<BeanCacheKey, Object>(); + + @Override + public void addMock(Object mockInstance, Annotation... qualifiers) + { + if (!TestBaseConfig.MockIntegration.ALLOW_MOCKED_BEANS && + !TestBaseConfig.MockIntegration.ALLOW_MOCKED_PRODUCERS) + { + throw new IllegalStateException("The support for mocked CDI-Beans is disabled " + + "due to a reduced portability across different CDI-implementations. " + + "Please set '" + TestBaseConfig.MockIntegration.ALLOW_MOCKED_BEANS_KEY + "' and/or '" + + TestBaseConfig.MockIntegration.ALLOW_MOCKED_PRODUCERS_KEY + "' to 'true' " + + "(in 'META-INF/apache-deltaspike.properties') on your test-classpath."); + } + + Class<?> mockClass = mockInstance.getClass(); + Class<?> beanClass = mockClass.getSuperclass(); + + if (beanClass == null) + { + beanClass = mockClass; + } + if (Object.class.equals(beanClass)) + { + throw new IllegalArgumentException(mockInstance.getClass().getName() + + " isn't a supported approach for mocking -> please extend from the original class."); + } + + TypedMock typedMock = mockClass.getAnnotation(TypedMock.class); + + if (typedMock == null) + { + typedMock = beanClass.getAnnotation(TypedMock.class); + } + + Class[] specifiedTypes = null; + + if (typedMock != null) + { + specifiedTypes = typedMock.value(); + } + else + { + Typed typed = mockClass.getAnnotation(Typed.class); + + if (typed == null || typed.value().length == 0) + { + typed = beanClass.getAnnotation(Typed.class); + } + + if (typed != null && typed.value().length > 0) + { + specifiedTypes = typed.value(); + } + } + + if (specifiedTypes != null) + { + for (Class typedClass : specifiedTypes) + { + this.registeredMocks.put(new BeanCacheKey(typedClass, qualifiers), mockInstance); + } + } + else + { + this.registeredMocks.put(new BeanCacheKey(beanClass, qualifiers), mockInstance); + } + } + + @Override + public <T> T getMock(Class<T> beanClass, Annotation... qualifiers) + { + return (T)this.registeredMocks.get(new BeanCacheKey(beanClass, qualifiers)); + } + + @Override + public void reset() + { + this.registeredMocks.clear(); + } +} diff --git a/deltaspike/modules/test-control5/impl/src/main/java/org/apache/deltaspike/testcontrol5/impl/mock/BeanCacheKey.java b/deltaspike/modules/test-control5/impl/src/main/java/org/apache/deltaspike/testcontrol5/impl/mock/BeanCacheKey.java new file mode 100644 index 000000000..0461a4d3f --- /dev/null +++ b/deltaspike/modules/test-control5/impl/src/main/java/org/apache/deltaspike/testcontrol5/impl/mock/BeanCacheKey.java @@ -0,0 +1,265 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.deltaspike.testcontrol5.impl.mock; + +import org.apache.deltaspike.core.util.ReflectionUtils; + +import jakarta.enterprise.util.Nonbinding; +import java.lang.annotation.Annotation; +import java.lang.reflect.Array; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.Comparator; + +public class BeanCacheKey +{ + private static final Comparator<Annotation> ANNOTATION_COMPARATOR = new AnnotationComparator(); + + private final Type type; + private final Annotation qualifier; + private final Annotation qualifiers[]; + private final int hashCode; + + public BeanCacheKey(Type type, Annotation... qualifiers) + { + this.type = type; + final int length = qualifiers != null ? qualifiers.length : 0; + if (length == 0) + { + qualifier = null; + this.qualifiers = null; + } + else if (length == 1) + { + qualifier = qualifiers[0]; + this.qualifiers = null; + } + else + { + qualifier = null; + this.qualifiers = new Annotation[length]; + System.arraycopy(qualifiers, 0, this.qualifiers, 0, length); + Arrays.sort(this.qualifiers, ANNOTATION_COMPARATOR); + } + + hashCode = computeHashCode(); + } + + @Override + public boolean equals(Object o) + { + if (this == o) + { + return true; + } + if (o == null || getClass() != o.getClass()) + { + return false; + } + + BeanCacheKey cacheKey = (BeanCacheKey) o; + + if (!type.equals(cacheKey.type)) + { + return false; + } + if (qualifier != null ? !qualifierEquals(qualifier, cacheKey.qualifier) : cacheKey.qualifier != null) + { + return false; + } + if (!qualifierArrayEquals(qualifiers, cacheKey.qualifiers)) + { + return false; + } + + return true; + } + + private boolean qualifierArrayEquals(Annotation[] qualifiers1, Annotation[] qualifiers2) + { + if (qualifiers1 == qualifiers2) + { + return true; + } + else if (qualifiers1 == null || qualifiers2 == null) + { + return false; + } + if (qualifiers1.length != qualifiers2.length) + { + return false; + } + for (int i = 0; i < qualifiers1.length; i++) + { + Annotation a1 = qualifiers1[i]; + Annotation a2 = qualifiers2[i]; + if (a1 == null ? a2 != null : !qualifierEquals(a1, a2)) + { + return false; + } + } + + return true; + } + + @Override + public int hashCode() + { + return hashCode; + } + + private int computeHashCode() + { + int computedHashCode = 31 * ReflectionUtils.calculateHashCodeOfType(type); + if (qualifier != null) + { + computedHashCode = 31 * computedHashCode + getQualifierHashCode(qualifier); + } + if (qualifiers != null) + { + for (int i = 0; i < qualifiers.length; i++) + { + computedHashCode = 31 * computedHashCode + getQualifierHashCode(qualifiers[i]); + } + } + return computedHashCode; + } + + private int getQualifierHashCode(Annotation a) + { + return ReflectionUtils.calculateHashCodeOfAnnotation(a, true); + } + + private boolean qualifierEquals(Annotation qualifier1, Annotation qualifier2) + { + return ANNOTATION_COMPARATOR.compare(qualifier1, qualifier2) == 0; + } + + @Override + public String toString() + { + return "BeanCacheKey{" + "type=" + type + ", qualifiers=" + + (qualifiers == null ? qualifier : Arrays.asList(qualifiers)) + ", hashCode=" + hashCode + '}'; + } + + private static class AnnotationComparator implements Comparator<Annotation> + { + @Override + public int compare(Annotation annotation1, Annotation annotation2) + { + final Class<? extends Annotation> type1 = annotation1.annotationType(); + final Class<? extends Annotation> type2 = annotation2.annotationType(); + final int temp = type1.getName().compareTo(type2.getName()); + if (temp != 0) + { + return temp; + } + final Method[] member1 = type1.getDeclaredMethods(); + final Method[] member2 = type2.getDeclaredMethods(); + + int i = 0; + int j = 0; + final int length1 = member1.length; + final int length2 = member2.length; + + for (;; i++, j++) + { + while (i < length1 && member1[i].isAnnotationPresent(Nonbinding.class)) + { + i++; + } + while (j < length2 && member2[j].isAnnotationPresent(Nonbinding.class)) + { + j++; + } + if (i >= length1 && j >= length2) + { + return 0; + } + else if (i >= length1) + { + return 1; + } + else if (j >= length2) + { + return -1; + } + else + { + int c = member1[i].getName().compareTo(member2[j].getName()); + if (c != 0) + { + return c; + } + final Object value1 = ReflectionUtils.invokeMethod(annotation1, member1[i], Object.class, true); + final Object value2 = ReflectionUtils.invokeMethod(annotation2, member2[j], Object.class, true); + assert value1.getClass().equals(value2.getClass()); + + if (value1 instanceof Comparable) + { + c = ((Comparable)value1).compareTo(value2); + if (c != 0) + { + return c; + } + } + else if (value1.getClass().isArray()) + { + c = value1.getClass().getComponentType().getName() + .compareTo(value2.getClass().getComponentType().getName()); + if (c != 0) + { + return c; + } + + final int length = Array.getLength(value1); + c = length - Array.getLength(value2); + if (c != 0) + { + return c; + } + for (int k = 0; k < length; k++) + { + c = ((Comparable)Array.get(value1, k)).compareTo(Array.get(value2, k)); + if (c != 0) + { + return c; + } + } + + } + else if (value1 instanceof Class) + { + + c = ((Class)value1).getName().compareTo(((Class) value2).getName()); + if (c != 0) + { + return c; + } + } + else + { + assert false; + } + } + } + } + } +} diff --git a/deltaspike/modules/test-control5/impl/src/main/java/org/apache/deltaspike/testcontrol5/impl/mock/DefaultMockFilter.java b/deltaspike/modules/test-control5/impl/src/main/java/org/apache/deltaspike/testcontrol5/impl/mock/DefaultMockFilter.java new file mode 100644 index 000000000..14044ad5c --- /dev/null +++ b/deltaspike/modules/test-control5/impl/src/main/java/org/apache/deltaspike/testcontrol5/impl/mock/DefaultMockFilter.java @@ -0,0 +1,172 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.deltaspike.testcontrol5.impl.mock; + +import org.apache.deltaspike.testcontrol5.api.junit.TestBaseConfig; +import org.apache.deltaspike.testcontrol5.spi.mock.MockFilter; + +import jakarta.enterprise.inject.Produces; +import jakarta.enterprise.inject.spi.Annotated; +import jakarta.enterprise.inject.spi.AnnotatedField; +import jakarta.enterprise.inject.spi.AnnotatedMember; +import jakarta.enterprise.inject.spi.AnnotatedMethod; +import jakarta.enterprise.inject.spi.AnnotatedType; +import jakarta.enterprise.inject.spi.BeanManager; +import java.lang.annotation.Annotation; +import java.lang.reflect.Member; +import java.util.HashSet; +import java.util.Set; +import java.util.logging.Logger; + +public class DefaultMockFilter implements MockFilter +{ + private static final Logger LOG = Logger.getLogger(DefaultMockFilter.class.getName()); + + private static final String DS_BASE_PACKAGE = "org.apache.deltaspike."; + private static final String JAVA_BASE_PACKAGE = "java."; + private static final String JAVAX_BASE_PACKAGE = "javax."; + private static final String JAKARTA_BASE_PACKAGE = "jakarta."; + private static final String EJB_BASE_PACKAGE = "javax.ejb."; + private static final String OWB_BASE_PACKAGE = "org.apache.webbeans."; + private static final String WELD_BASE_PACKAGE = "org.jboss.weld."; + + @Override + public boolean isMockedImplementationSupported(BeanManager beanManager, Annotated annotated) + { + if (!isMockSupportEnabled(annotated)) + { + return false; + } + + Class origin = null; + if (annotated instanceof AnnotatedType) + { + origin = ((AnnotatedType)annotated).getJavaClass(); + Set<Annotation> annotations = new HashSet<Annotation>(); + annotations.addAll(annotated.getAnnotations()); + + for (AnnotatedMethod annotatedMethod : + (Set<jakarta.enterprise.inject.spi.AnnotatedMethod>)((AnnotatedType) annotated).getMethods()) + { + annotations.addAll(annotatedMethod.getAnnotations()); + } + + if (isEjbOrAnnotatedTypeWithInterceptorAnnotation( + beanManager, annotations, origin.getName())) + { + return false; + } + } + else if (annotated instanceof AnnotatedMember) + { + Member member = ((AnnotatedMember)annotated).getJavaMember(); + origin = member.getDeclaringClass(); + if (isEjbOrAnnotatedTypeWithInterceptorAnnotation( + beanManager, annotated.getAnnotations(), member.toString())) + { + return false; + } + } + + if (origin != null && origin.getPackage() == null) + { + LOG.warning("Please don't use the default-package for " + origin.getName()); + return true; + } + + return origin != null && !isInternalPackage(origin.getPackage().getName()); + } + + protected boolean isMockSupportEnabled(Annotated annotated) + { + if ((annotated instanceof AnnotatedMethod || annotated instanceof AnnotatedField) && + annotated.getAnnotation(Produces.class) != null) + { + return TestBaseConfig.MockIntegration.ALLOW_MOCKED_PRODUCERS; + } + else + { + return TestBaseConfig.MockIntegration.ALLOW_MOCKED_BEANS; + } + } + + protected boolean isEjbOrAnnotatedTypeWithInterceptorAnnotation(BeanManager beanManager, + Set<Annotation> annotations, + String origin) + { + for (Annotation annotation : annotations) + { + if (annotation.annotationType().getName().startsWith(EJB_BASE_PACKAGE)) + { + return true; + } + + if (isStandardAnnotation(annotation)) + { + continue; + } + + if (beanManager.isInterceptorBinding(annotation.annotationType()) || + (beanManager.isStereotype(annotation.annotationType()) && + isStereotypeWithInterceptor(annotation, beanManager))) + { + LOG.warning("Skip mocking intercepted bean " + origin); + + return true; + } + } + return false; + } + + protected boolean isStereotypeWithInterceptor(Annotation stereotypeAnnotation, BeanManager beanManager) + { + for (Annotation annotation : stereotypeAnnotation.annotationType().getAnnotations()) + { + if (isStandardAnnotation(annotation)) + { + continue; + } + + if (beanManager.isInterceptorBinding(annotation.annotationType()) || + isStereotypeWithInterceptor(annotation, beanManager)) + { + return true; + } + } + return false; + } + + protected boolean isStandardAnnotation(Annotation annotation) + { + return annotation.annotationType().getName().startsWith(JAVA_BASE_PACKAGE) || + annotation.annotationType().getName().startsWith(JAVAX_BASE_PACKAGE) || + annotation.annotationType().getName().startsWith(JAKARTA_BASE_PACKAGE); + } + + protected boolean isInternalPackage(String packageName) + { + return packageName.startsWith(OWB_BASE_PACKAGE) || packageName.startsWith(WELD_BASE_PACKAGE) || + isDeltaSpikePackage(packageName); + } + + protected boolean isDeltaSpikePackage(String packageName) + { + return packageName.startsWith(DS_BASE_PACKAGE); + } +} diff --git a/deltaspike/modules/test-control5/impl/src/main/java/org/apache/deltaspike/testcontrol5/impl/mock/MockAwareInjectionTargetWrapper.java b/deltaspike/modules/test-control5/impl/src/main/java/org/apache/deltaspike/testcontrol5/impl/mock/MockAwareInjectionTargetWrapper.java new file mode 100644 index 000000000..c7e5246cf --- /dev/null +++ b/deltaspike/modules/test-control5/impl/src/main/java/org/apache/deltaspike/testcontrol5/impl/mock/MockAwareInjectionTargetWrapper.java @@ -0,0 +1,99 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.deltaspike.testcontrol5.impl.mock; + +import org.apache.deltaspike.core.api.provider.BeanProvider; +import org.apache.deltaspike.testcontrol5.api.mock.DynamicMockManager; + +import jakarta.enterprise.context.spi.CreationalContext; +import jakarta.enterprise.inject.spi.BeanManager; +import jakarta.enterprise.inject.spi.InjectionPoint; +import jakarta.enterprise.inject.spi.InjectionTarget; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.util.List; +import java.util.Set; + +public class MockAwareInjectionTargetWrapper<T> implements InjectionTarget<T> +{ + private BeanManager beanManager; + private final InjectionTarget<T> wrapped; + private final List<Type> beanTypes; + private final List<Annotation> qualifiers; + + public MockAwareInjectionTargetWrapper(BeanManager beanManager, + InjectionTarget<T> wrapped, + List<Type> beanTypes, + List<Annotation> qualifiers) + { + this.beanManager = beanManager; + this.wrapped = wrapped; + this.beanTypes = beanTypes; + this.qualifiers = qualifiers; + } + + @Override + public T produce(CreationalContext<T> creationalContext) + { + DynamicMockManager mockManager = + BeanProvider.getContextualReference(this.beanManager, DynamicMockManager.class, false); + + for (Type beanType : this.beanTypes) + { + Object mockInstance = mockManager.getMock( + (Class)beanType, this.qualifiers.toArray(new Annotation[this.qualifiers.size()])); + + if (mockInstance != null) + { + return (T)mockInstance; + } + } + return wrapped.produce(creationalContext); + } + + @Override + public void inject(T instance, CreationalContext<T> ctx) + { + wrapped.inject(instance, ctx); + } + + @Override + public void postConstruct(T instance) + { + wrapped.postConstruct(instance); + } + + @Override + public void preDestroy(T instance) + { + wrapped.preDestroy(instance); + } + + @Override + public void dispose(T instance) + { + wrapped.dispose(instance); + } + + @Override + public Set<InjectionPoint> getInjectionPoints() + { + return wrapped.getInjectionPoints(); + } +} diff --git a/deltaspike/modules/test-control5/impl/src/main/java/org/apache/deltaspike/testcontrol5/impl/mock/MockAwareProducerWrapper.java b/deltaspike/modules/test-control5/impl/src/main/java/org/apache/deltaspike/testcontrol5/impl/mock/MockAwareProducerWrapper.java new file mode 100644 index 000000000..0b4ec4653 --- /dev/null +++ b/deltaspike/modules/test-control5/impl/src/main/java/org/apache/deltaspike/testcontrol5/impl/mock/MockAwareProducerWrapper.java @@ -0,0 +1,82 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.deltaspike.testcontrol5.impl.mock; + +import org.apache.deltaspike.core.api.provider.BeanProvider; +import org.apache.deltaspike.testcontrol5.api.mock.DynamicMockManager; + +import jakarta.enterprise.context.spi.CreationalContext; +import jakarta.enterprise.inject.spi.BeanManager; +import jakarta.enterprise.inject.spi.InjectionPoint; +import jakarta.enterprise.inject.spi.Producer; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.util.List; +import java.util.Set; + +public class MockAwareProducerWrapper<T> implements Producer<T> +{ + private final BeanManager beanManager; + private final Producer<T> wrapped; + private final List<Type> beanTypes; + private final List<Annotation> qualifiers; + + public MockAwareProducerWrapper(BeanManager beanManager, + Producer<T> wrapped, + List<Type> beanTypes, + List<Annotation> qualifiers) + { + this.beanManager = beanManager; + this.wrapped = wrapped; + this.beanTypes = beanTypes; + this.qualifiers = qualifiers; + } + + @Override + public T produce(CreationalContext<T> creationalContext) + { + DynamicMockManager mockManager = + BeanProvider.getContextualReference(this.beanManager, DynamicMockManager.class, false); + + for (Type beanType : this.beanTypes) + { + Object mockInstance = mockManager.getMock( + (Class)beanType, this.qualifiers.toArray(new Annotation[this.qualifiers.size()])); + + if (mockInstance != null) + { + return (T)mockInstance; + } + } + + return wrapped.produce(creationalContext); + } + + @Override + public void dispose(T instance) + { + wrapped.dispose(instance); + } + + @Override + public Set<InjectionPoint> getInjectionPoints() + { + return wrapped.getInjectionPoints(); + } +} diff --git a/deltaspike/modules/test-control5/impl/src/main/java/org/apache/deltaspike/testcontrol5/impl/mock/MockExtension.java b/deltaspike/modules/test-control5/impl/src/main/java/org/apache/deltaspike/testcontrol5/impl/mock/MockExtension.java new file mode 100644 index 000000000..b94d29604 --- /dev/null +++ b/deltaspike/modules/test-control5/impl/src/main/java/org/apache/deltaspike/testcontrol5/impl/mock/MockExtension.java @@ -0,0 +1,157 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.deltaspike.testcontrol5.impl.mock; + +import jakarta.enterprise.inject.Typed; +import org.apache.deltaspike.core.spi.activation.Deactivatable; +import org.apache.deltaspike.core.util.ClassDeactivationUtils; +import org.apache.deltaspike.core.util.ServiceUtils; +import org.apache.deltaspike.testcontrol5.spi.mock.MockFilter; + +import jakarta.enterprise.event.Observes; +import jakarta.enterprise.inject.spi.AnnotatedMember; +import jakarta.enterprise.inject.spi.BeanManager; +import jakarta.enterprise.inject.spi.BeforeBeanDiscovery; +import jakarta.enterprise.inject.spi.Extension; +import jakarta.enterprise.inject.spi.InjectionTarget; +import jakarta.enterprise.inject.spi.ProcessInjectionTarget; +import jakarta.enterprise.inject.spi.ProcessProducer; +import jakarta.enterprise.inject.spi.Producer; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class MockExtension implements Extension, Deactivatable +{ + private Boolean isActivated = true; + private List<MockFilter> mockFilters; + + protected void init(@Observes BeforeBeanDiscovery beforeBeanDiscovery) + { + isActivated = ClassDeactivationUtils.isActivated(getClass()); + mockFilters = ServiceUtils.loadServiceImplementations(MockFilter.class); + } + + public <X> void onProcessInjectionTarget(@Observes ProcessInjectionTarget<X> processInjectionTarget, + BeanManager beanManager) + { + if (!isActivated) + { + return; + } + + for (MockFilter mockFilter : mockFilters) + { + if (!mockFilter.isMockedImplementationSupported(beanManager, processInjectionTarget.getAnnotatedType())) + { + return; + } + } + + List<Annotation> qualifiers = new ArrayList<Annotation>(); + for (Annotation annotation : processInjectionTarget.getAnnotatedType().getAnnotations()) + { + if (beanManager.isQualifier(annotation.annotationType())) + { + qualifiers.add(annotation); + } + } + + Typed typed = processInjectionTarget.getAnnotatedType().getAnnotation(Typed.class); + + List<Type> foundTypes = new ArrayList<>(); + if (typed != null) + { + Collections.addAll(foundTypes, typed.value()); + } + else + { + foundTypes.addAll(extractTypes(processInjectionTarget.getAnnotatedType().getJavaClass())); + } + + if (foundTypes.isEmpty()) + { + return; + } + + final InjectionTarget<X> originalInjectionTarget = processInjectionTarget.getInjectionTarget(); + processInjectionTarget.setInjectionTarget(new MockAwareInjectionTargetWrapper<X>( + beanManager, originalInjectionTarget, foundTypes, qualifiers)); + } + + public <X, T> void onProcessProducer(@Observes ProcessProducer<X, T> processProducer, BeanManager beanManager) + { + if (!isActivated) + { + return; + } + + for (MockFilter mockFilter : mockFilters) + { + if (!mockFilter.isMockedImplementationSupported(beanManager, processProducer.getAnnotatedMember())) + { + return; + } + } + + final Producer<T> originalProducer = processProducer.getProducer(); + AnnotatedMember<X> annotatedMember = processProducer.getAnnotatedMember(); + List<Annotation> qualifiers = new ArrayList<Annotation>(); + for (Annotation annotation : annotatedMember.getAnnotations()) + { + if (beanManager.isQualifier(annotation.annotationType())) + { + qualifiers.add(annotation); + } + } + + Typed typed = annotatedMember.getAnnotation(Typed.class); + + List<Type> foundTypes = new ArrayList<Type>(); + if (typed != null) + { + Collections.addAll(foundTypes, typed.value()); + } + else if (annotatedMember.getBaseType() instanceof Class) + { + foundTypes.addAll(extractTypes((Class)annotatedMember.getBaseType())); + } + + if (foundTypes.isEmpty()) + { + return; + } + + processProducer.setProducer(new MockAwareProducerWrapper<T>( + beanManager, originalProducer, foundTypes, qualifiers)); + } + + protected List<Type> extractTypes(Class currentClass) + { + List<Type> result = new ArrayList<Type>(); + for (Class<?> c = currentClass; c != Object.class && c != null; c = c.getSuperclass()) + { + result.add(c); + } + Collections.addAll(result, currentClass.getInterfaces()); + return result; + } +} diff --git a/deltaspike/modules/test-control5/impl/src/main/java/org/apache/deltaspike/testcontrol5/impl/mock/SimpleApplicationMockManager.java b/deltaspike/modules/test-control5/impl/src/main/java/org/apache/deltaspike/testcontrol5/impl/mock/SimpleApplicationMockManager.java new file mode 100644 index 000000000..d3dd4439a --- /dev/null +++ b/deltaspike/modules/test-control5/impl/src/main/java/org/apache/deltaspike/testcontrol5/impl/mock/SimpleApplicationMockManager.java @@ -0,0 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.deltaspike.testcontrol5.impl.mock; + +import jakarta.enterprise.inject.Typed; +import org.apache.deltaspike.testcontrol5.api.mock.ApplicationMockManager; + +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +@Typed(ApplicationMockManager.class) +public class SimpleApplicationMockManager extends AbstractMockManager implements ApplicationMockManager +{ +} diff --git a/deltaspike/modules/test-control5/impl/src/main/java/org/apache/deltaspike/testcontrol5/impl/mock/SimpleMockManager.java b/deltaspike/modules/test-control5/impl/src/main/java/org/apache/deltaspike/testcontrol5/impl/mock/SimpleMockManager.java new file mode 100644 index 000000000..f00699aff --- /dev/null +++ b/deltaspike/modules/test-control5/impl/src/main/java/org/apache/deltaspike/testcontrol5/impl/mock/SimpleMockManager.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.deltaspike.testcontrol5.impl.mock; + +import jakarta.enterprise.inject.Typed; +import org.apache.deltaspike.testcontrol5.api.mock.ApplicationMockManager; +import org.apache.deltaspike.testcontrol5.api.mock.DynamicMockManager; + +import jakarta.enterprise.context.RequestScoped; +import jakarta.inject.Inject; +import java.lang.annotation.Annotation; + +@RequestScoped +@Typed(DynamicMockManager.class) +public class SimpleMockManager extends AbstractMockManager +{ + @Inject + private ApplicationMockManager applicationMockManager; + + @Override + public <T> T getMock(Class<T> beanClass, Annotation... qualifiers) + { + T result = applicationMockManager.getMock(beanClass, qualifiers); + + if (result != null) + { + return result; + } + return super.getMock(beanClass, qualifiers); + } +} diff --git a/deltaspike/modules/test-control5/impl/src/main/java/org/apache/deltaspike/testcontrol5/impl/request/ContextControlDecorator.java b/deltaspike/modules/test-control5/impl/src/main/java/org/apache/deltaspike/testcontrol5/impl/request/ContextControlDecorator.java new file mode 100644 index 000000000..32e6882a1 --- /dev/null +++ b/deltaspike/modules/test-control5/impl/src/main/java/org/apache/deltaspike/testcontrol5/impl/request/ContextControlDecorator.java @@ -0,0 +1,115 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.deltaspike.testcontrol5.impl.request; + +import org.apache.deltaspike.cdise.api.ContextControl; +import org.apache.deltaspike.testcontrol5.api.junit.CdiTestExtension; +import org.apache.deltaspike.testcontrol5.spi.ExternalContainer; + +import jakarta.decorator.Decorator; +import jakarta.decorator.Delegate; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.context.ConversationScoped; +import jakarta.enterprise.context.RequestScoped; +import jakarta.enterprise.context.SessionScoped; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import java.lang.annotation.Annotation; + +@Decorator +public class ContextControlDecorator implements ContextControl +{ + @Inject + @Delegate + private ContextControl wrapped; + + @Override + public void startContexts() + { + wrapped.startContexts(); + + if (isManualScopeHandling()) + { + for (ExternalContainer externalContainer : CdiTestExtension.getActiveExternalContainers()) + { + externalContainer.startScope(Singleton.class); + externalContainer.startScope(ApplicationScoped.class); + externalContainer.startScope(RequestScoped.class); + externalContainer.startScope(SessionScoped.class); + externalContainer.startScope(ConversationScoped.class); + } + } + } + + @Override + public void stopContexts() + { + if (isManualScopeHandling()) + { + for (ExternalContainer externalContainer : CdiTestExtension.getActiveExternalContainers()) + { + externalContainer.stopScope(ConversationScoped.class); + externalContainer.stopScope(SessionScoped.class); + externalContainer.stopScope(RequestScoped.class); + externalContainer.stopScope(ApplicationScoped.class); + externalContainer.stopScope(Singleton.class); + } + } + + wrapped.stopContexts(); + } + + @Override + public void startContext(Class<? extends Annotation> scopeClass) + { + wrapped.startContext(scopeClass); + + if (isManuallyHandledRequest(scopeClass)) + { + for (ExternalContainer externalContainer : CdiTestExtension.getActiveExternalContainers()) + { + externalContainer.startScope(scopeClass); + } + } + } + + @Override + public void stopContext(Class<? extends Annotation> scopeClass) + { + wrapped.stopContext(scopeClass); + + if (isManuallyHandledRequest(scopeClass)) + { + for (ExternalContainer externalContainer : CdiTestExtension.getActiveExternalContainers()) + { + externalContainer.stopScope(scopeClass); + } + } + } + + private boolean isManuallyHandledRequest(Class<? extends Annotation> scopeClass) + { + return RequestScoped.class.equals(scopeClass) && isManualScopeHandling(); + } + + private boolean isManualScopeHandling() + { + return !Boolean.TRUE.equals(CdiTestExtension.isAutomaticScopeHandlingActive()); + } +} diff --git a/deltaspike/modules/test-control5/impl/src/main/java/org/apache/deltaspike/testcontrol5/impl/validation/StandardContextTestControlValidator.java b/deltaspike/modules/test-control5/impl/src/main/java/org/apache/deltaspike/testcontrol5/impl/validation/StandardContextTestControlValidator.java new file mode 100644 index 000000000..35461e9a6 --- /dev/null +++ b/deltaspike/modules/test-control5/impl/src/main/java/org/apache/deltaspike/testcontrol5/impl/validation/StandardContextTestControlValidator.java @@ -0,0 +1,107 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.deltaspike.testcontrol5.impl.validation; + +import org.apache.deltaspike.cdise.api.CdiContainerLoader; +import org.apache.deltaspike.testcontrol5.api.TestControl; +import org.apache.deltaspike.testcontrol5.spi.TestAware; +import org.apache.deltaspike.testcontrol5.spi.TestControlValidator; + +import jakarta.enterprise.inject.Vetoed; +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +@Vetoed +public class StandardContextTestControlValidator implements TestAware, TestControlValidator +{ + private static Boolean customContextControlDetected; + + private static ThreadLocal<Class> currentTestClass = new ThreadLocal<Class>(); + private static ThreadLocal<Method> currentTestMethod = new ThreadLocal<Method>(); + + @Override + public void validate(TestControl testControl) + { + checkActiveContextControlImplementation(); + + List<Class<? extends Annotation>> scopeClasses = new ArrayList<Class<? extends Annotation>>(); + Collections.addAll(scopeClasses, testControl.startScopes()); + + validateSupportedScopes(scopeClasses, currentTestClass.get(), currentTestMethod.get()); + } + + private void checkActiveContextControlImplementation() + { + if (customContextControlDetected != null) + { + return; + } + + customContextControlDetected = !CdiContainerLoader.getCdiContainer().getContextControl() + .getClass().getName().startsWith("org.apache.deltaspike."); + } + + private void validateSupportedScopes(List<Class<? extends Annotation>> scopeClasses, + Class<?> declaringClass, + Method testMethod) + { + if (Boolean.TRUE.equals(customContextControlDetected)) + { + return; + } + + for (Class<? extends Annotation> scopeClass : scopeClasses) + { + if (!scopeClass.getName().startsWith("jakarta.enterprise.context.")) + { + throw new IllegalStateException("Please remove " + scopeClass.getName() + " at " + declaringClass + + (testMethod != null ? "#" + testMethod.getName() : "") + + " from @" + TestControl.class.getName() + ". @" + TestControl.class.getName() + + " only supports standard Scope-Annotations provided by the CDI-Specification. " + + "Other Contexts start automatically or need to get started with a specific Management-API. " + + "Examples: " + + "@TransactionScoped gets started automatically once the @Transactional-Interceptor is used. " + + "Whereas @WindowScoped starts once WindowContext#activateWindow gets called."); + } + } + } + + @Override + public void setTestClass(Class testClass) + { + currentTestClass.set(testClass); + if (testClass == null) + { + currentTestClass.remove(); + } + } + + @Override + public void setTestMethod(Method testMethod) + { + currentTestMethod.set(testMethod); + if (testMethod == null) + { + currentTestMethod.remove(); + } + } +} diff --git a/deltaspike/modules/pom.xml b/deltaspike/modules/test-control5/pom.xml similarity index 70% copy from deltaspike/modules/pom.xml copy to deltaspike/modules/test-control5/pom.xml index d4017033f..7d230af65 100644 --- a/deltaspike/modules/pom.xml +++ b/deltaspike/modules/test-control5/pom.xml @@ -21,27 +21,20 @@ <modelVersion>4.0.0</modelVersion> <parent> - <groupId>org.apache.deltaspike</groupId> - <artifactId>parent-code</artifactId> + <groupId>org.apache.deltaspike.modules</groupId> + <artifactId>modules-project</artifactId> <version>2.0.2-SNAPSHOT</version> - <relativePath>../parent/code/pom.xml</relativePath> + <relativePath>../pom.xml</relativePath> </parent> <groupId>org.apache.deltaspike.modules</groupId> - <artifactId>modules-project</artifactId> - <version>2.0.2-SNAPSHOT</version> + <artifactId>test-control5-module-project</artifactId> <packaging>pom</packaging> - <name>Apache DeltaSpike Modules</name> + <name>Apache DeltaSpike Test-Control5-Module (JUnit 5)</name> <modules> - <module>proxy</module> - <module>security</module> - <module>jsf</module> - <module>partial-bean</module> - <module>jpa</module> - <module>data</module> - <module>scheduler</module> - <module>test-control</module> + <module>api</module> + <module>impl</module> </modules> </project>
