diff --git a/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/HelidonJunitExtension.java b/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/HelidonJunitExtension.java index 5599a1bc9fe..a046abd318c 100644 --- a/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/HelidonJunitExtension.java +++ b/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/HelidonJunitExtension.java @@ -30,11 +30,13 @@ import java.net.URL; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; import io.helidon.config.mp.MpConfigSources; import io.helidon.microprofile.server.JaxRsCdiExtension; @@ -88,7 +90,8 @@ class HelidonJunitExtension implements BeforeAllCallback, InvocationInterceptor, ParameterResolver { private static final Set> HELIDON_TEST_ANNOTATIONS = - Set.of(AddBean.class, AddConfig.class, AddExtension.class, Configuration.class, AddJaxRs.class); + Set.of(AddBean.class, AddConfig.class, AddExtension.class, Configuration.class, + AddJaxRs.class, AddConfigBlock.class); private static final Map, Annotation> BEAN_DEFINING = new HashMap<>(); static { @@ -115,16 +118,18 @@ class HelidonJunitExtension implements BeforeAllCallback, public void beforeAll(ExtensionContext context) { testClass = context.getRequiredTestClass(); - AddConfig[] configs = getAnnotations(testClass, AddConfig.class); + List metaAnnotations = extractMetaAnnotations(testClass); + + AddConfig[] configs = getAnnotations(testClass, AddConfig.class, metaAnnotations); classLevelConfigMeta.addConfig(configs); - classLevelConfigMeta.configuration(testClass.getAnnotation(Configuration.class)); - classLevelConfigMeta.addConfigBlock(testClass.getAnnotation(AddConfigBlock.class)); + classLevelConfigMeta.configuration(getAnnotation(testClass, Configuration.class, metaAnnotations)); + classLevelConfigMeta.addConfigBlock(getAnnotation(testClass, AddConfigBlock.class, metaAnnotations)); configProviderResolver = ConfigProviderResolver.instance(); - AddExtension[] extensions = getAnnotations(testClass, AddExtension.class); + AddExtension[] extensions = getAnnotations(testClass, AddExtension.class, metaAnnotations); classLevelExtensions.addAll(Arrays.asList(extensions)); - AddBean[] beans = getAnnotations(testClass, AddBean.class); + AddBean[] beans = getAnnotations(testClass, AddBean.class, metaAnnotations); classLevelBeans.addAll(Arrays.asList(beans)); HelidonTest testAnnot = testClass.getAnnotation(HelidonTest.class); @@ -132,7 +137,7 @@ public void beforeAll(ExtensionContext context) { resetPerTest = testAnnot.resetPerTest(); } - DisableDiscovery discovery = testClass.getAnnotation(DisableDiscovery.class); + DisableDiscovery discovery = getAnnotation(testClass, DisableDiscovery.class, metaAnnotations); if (discovery != null) { classLevelDisableDiscovery = discovery.value(); } @@ -145,7 +150,7 @@ public void beforeAll(ExtensionContext context) { validatePerClass(); // add beans when using JaxRS - AddJaxRs addJaxRsAnnotation = testClass.getAnnotation(AddJaxRs.class); + AddJaxRs addJaxRsAnnotation = getAnnotation(testClass, AddJaxRs.class, metaAnnotations); if (addJaxRsAnnotation != null){ classLevelExtensions.add(ProcessAllAnnotatedTypesLiteral.INSTANCE); classLevelExtensions.add(ServerCdiExtensionLiteral.INSTANCE); @@ -164,13 +169,42 @@ public void beforeAll(ExtensionContext context) { } } + private List extractMetaAnnotations(Class testClass) { + Annotation[] testAnnotations = testClass.getAnnotations(); + for (Annotation testAnnotation : testAnnotations) { + List annotations = List.of(testAnnotation.annotationType().getAnnotations()); + List> annotationsClass = annotations.stream() + .map(a -> a.annotationType()).collect(Collectors.toList()); + if (!Collections.disjoint(HELIDON_TEST_ANNOTATIONS, annotationsClass)) { + // Contains at least one of HELIDON_TEST_ANNOTATIONS + return annotations; + } + } + return List.of(); + } + + private T getAnnotation(Class testClass, Class annotClass, + List metaAnnotations) { + T annotation = testClass.getAnnotation(annotClass); + if (annotation == null) { + List byType = annotationsByType(annotClass, metaAnnotations); + if (!byType.isEmpty()) { + annotation = byType.get(0); + } + } + return annotation; + } + @SuppressWarnings("unchecked") - private T[] getAnnotations(Class testClass, Class annotClass) { + private T[] getAnnotations(Class testClass, Class annotClass, + List metaAnnotations) { // inherited does not help, as it only returns annot from superclass if // child has none T[] directAnnotations = testClass.getAnnotationsByType(annotClass); List allAnnotations = new ArrayList<>(List.of(directAnnotations)); + // Include meta annotations + allAnnotations.addAll(annotationsByType(annotClass, metaAnnotations)); Class superClass = testClass.getSuperclass(); while (superClass != null) { @@ -187,6 +221,16 @@ private T[] getAnnotations(Class testClass, Class a return (T[]) result; } + private List annotationsByType(Class annotClass, List metaAnnotations) { + List byType = new ArrayList<>(); + for (Annotation annotation : metaAnnotations) { + if (annotation.annotationType() == annotClass) { + byType.add((T) annotation); + } + } + return byType; + } + @Override public void beforeEach(ExtensionContext context) throws Exception { if (resetPerTest) { diff --git a/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/HelidonTestNgListener.java b/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/HelidonTestNgListener.java index b2624adace4..a90b6b38492 100644 --- a/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/HelidonTestNgListener.java +++ b/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/HelidonTestNgListener.java @@ -28,11 +28,13 @@ import java.net.URL; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; import io.helidon.config.mp.MpConfigSources; import io.helidon.microprofile.server.JaxRsCdiExtension; @@ -73,8 +75,9 @@ */ public class HelidonTestNgListener implements IClassListener, ITestListener { - private static final Set> TEST_ANNOTATIONS = Set.of( - AddBean.class, AddConfig.class, AddExtension.class, Configuration.class, AddJaxRs.class); + private static final Set> TEST_ANNOTATIONS = + Set.of(AddBean.class, AddConfig.class, AddExtension.class, + Configuration.class, AddJaxRs.class, AddConfigBlock.class); private static final Map, AnnotationLiteral> BEAN_DEFINING = Map.of( ApplicationScoped.class, ApplicationScoped.Literal.INSTANCE, @@ -87,6 +90,7 @@ public class HelidonTestNgListener implements IClassListener, ITestListener { private ConfigMeta classLevelConfigMeta = new ConfigMeta(); private boolean classLevelDisableDiscovery = false; private boolean resetPerTest; + private Class testClass; private Object testInstance; private ConfigProviderResolver configProviderResolver; @@ -95,18 +99,21 @@ public class HelidonTestNgListener implements IClassListener, ITestListener { @Override public void onBeforeClass(ITestClass iTestClass) { + testClass = iTestClass.getRealClass(); - AddConfig[] configs = getAnnotations(testClass, AddConfig.class); + List metaAnnotations = extractMetaAnnotations(testClass); + + AddConfig[] configs = getAnnotations(testClass, AddConfig.class, metaAnnotations); classLevelConfigMeta.addConfig(configs); - classLevelConfigMeta.configuration(testClass.getAnnotation(Configuration.class)); - classLevelConfigMeta.addConfigBlock(testClass.getAnnotation(AddConfigBlock.class)); + classLevelConfigMeta.configuration(getAnnotation(testClass, Configuration.class, metaAnnotations)); + classLevelConfigMeta.addConfigBlock(getAnnotation(testClass, AddConfigBlock.class, metaAnnotations)); configProviderResolver = ConfigProviderResolver.instance(); - AddExtension[] extensions = getAnnotations(testClass, AddExtension.class); + AddExtension[] extensions = getAnnotations(testClass, AddExtension.class, metaAnnotations); classLevelExtensions.addAll(Arrays.asList(extensions)); - AddBean[] beans = getAnnotations(testClass, AddBean.class); + AddBean[] beans = getAnnotations(testClass, AddBean.class, metaAnnotations); classLevelBeans.addAll(Arrays.asList(beans)); HelidonTest testAnnot = testClass.getAnnotation(HelidonTest.class); @@ -114,7 +121,7 @@ public void onBeforeClass(ITestClass iTestClass) { resetPerTest = testAnnot.resetPerTest(); } - DisableDiscovery discovery = testClass.getAnnotation(DisableDiscovery.class); + DisableDiscovery discovery = getAnnotation(testClass, DisableDiscovery.class, metaAnnotations); if (discovery != null) { classLevelDisableDiscovery = discovery.value(); } @@ -126,7 +133,7 @@ public void onBeforeClass(ITestClass iTestClass) { validatePerClass(); // add beans when using JaxRS - AddJaxRs addJaxRsAnnotation = testClass.getAnnotation(AddJaxRs.class); + AddJaxRs addJaxRsAnnotation = getAnnotation(testClass, AddJaxRs.class, metaAnnotations); if (addJaxRsAnnotation != null){ classLevelExtensions.add(ProcessAllAnnotatedTypesLiteral.INSTANCE); classLevelExtensions.add(ServerCdiExtensionLiteral.INSTANCE); @@ -147,6 +154,7 @@ public void onBeforeClass(ITestClass iTestClass) { } } + @Override public void onAfterClass(ITestClass testClass) { if (!resetPerTest) { @@ -157,6 +165,7 @@ public void onAfterClass(ITestClass testClass) { @Override public void onTestStart(ITestResult result) { + if (resetPerTest) { Method method = result.getMethod().getConstructorOrMethod().getMethod(); AddConfig[] configs = method.getAnnotationsByType(AddConfig.class); @@ -268,6 +277,7 @@ private void releaseConfig() { if (configProviderResolver != null && config != null) { configProviderResolver.releaseConfig(config); config = null; + classLevelExtensions = new ArrayList<>(); classLevelBeans = new ArrayList<>(); classLevelConfigMeta = new ConfigMeta(); @@ -312,13 +322,52 @@ private void stopContainer() { } } + private List extractMetaAnnotations(Class testClass) { + Annotation[] testAnnotations = testClass.getAnnotations(); + for (Annotation testAnnotation : testAnnotations) { + List annotations = List.of(testAnnotation.annotationType().getAnnotations()); + List> annotationsClass = annotations.stream() + .map(a -> a.annotationType()).collect(Collectors.toList()); + if (!Collections.disjoint(TEST_ANNOTATIONS, annotationsClass)) { + // Contains at least one of HELIDON_TEST_ANNOTATIONS + return annotations; + } + } + return List.of(); + } + + private List annotationsByType(Class annotClass, List metaAnnotations) { + List byType = new ArrayList<>(); + for (Annotation annotation : metaAnnotations) { + if (annotation.annotationType() == annotClass) { + byType.add((T) annotation); + } + } + return byType; + } + + private T getAnnotation(Class testClass, Class annotClass, + List metaAnnotations) { + T annotation = testClass.getAnnotation(annotClass); + if (annotation == null) { + List byType = annotationsByType(annotClass, metaAnnotations); + if (!byType.isEmpty()) { + annotation = byType.get(0); + } + } + return annotation; + } + @SuppressWarnings("unchecked") - private T[] getAnnotations(Class testClass, Class annotClass) { + private T[] getAnnotations(Class testClass, Class annotClass, + List metaAnnotations) { // inherited does not help, as it only returns annot from superclass if // child has none T[] directAnnotations = testClass.getAnnotationsByType(annotClass); List allAnnotations = new ArrayList<>(List.of(directAnnotations)); + // Include meta annotations + allAnnotations.addAll(annotationsByType(annotClass, metaAnnotations)); Class superClass = testClass.getSuperclass(); while (superClass != null) { @@ -406,7 +455,7 @@ void registerOtherBeans(@Observes AfterBeanDiscovery event) { .addType(jakarta.ws.rs.client.WebTarget.class) .scope(ApplicationScoped.class) .produceWith(context -> ClientBuilder.newClient().target(clientUri())); - } + } void registerAddedBeans(@Observes BeforeBeanDiscovery event) { for (AddBean beanDef : addBeans) { @@ -461,12 +510,12 @@ public void inject(T testInstance, CreationalContext cc) { @Override public void postConstruct(T testInstance) { delegate.postConstruct(testInstance); - } + } @Override public void preDestroy(T testInstance) { delegate.preDestroy(testInstance); - } + } @Override public T produce(CreationalContext cc) { @@ -521,10 +570,12 @@ void addConfigBlock(AddConfigBlock config) { ConfigMeta nextMethod() { ConfigMeta methodMeta = new ConfigMeta(); + methodMeta.additionalKeys.putAll(this.additionalKeys); methodMeta.additionalSources.addAll(this.additionalSources); methodMeta.useExisting = this.useExisting; methodMeta.profile = this.profile; + return methodMeta; } } diff --git a/microprofile/tests/testing/junit5/src/test/java/io/helidon/microprofile/tests/testing/junit5/TestMetaAnnotation.java b/microprofile/tests/testing/junit5/src/test/java/io/helidon/microprofile/tests/testing/junit5/TestMetaAnnotation.java new file mode 100644 index 00000000000..5720506120f --- /dev/null +++ b/microprofile/tests/testing/junit5/src/test/java/io/helidon/microprofile/tests/testing/junit5/TestMetaAnnotation.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.tests.testing.junit5; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +import jakarta.inject.Inject; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import io.helidon.microprofile.testing.junit5.AddBean; +import io.helidon.microprofile.testing.junit5.AddConfig; +import io.helidon.microprofile.testing.junit5.AddConfigBlock; +import io.helidon.microprofile.testing.junit5.Configuration; +import io.helidon.microprofile.testing.junit5.HelidonTest; + +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.junit.jupiter.api.Test; + +@TestMetaAnnotation.MetaAnnotation +// @HelidonTest is still mandatory in the test and it has no effect in the MetaAnnotation +@HelidonTest +class TestMetaAnnotation { + + @Inject + MyBean bean; + + @Inject + @ConfigProperty(name = "some.key1") + private String value1; + + @Inject + @ConfigProperty(name = "some.key2") + private String value2; + + @Inject + @ConfigProperty(name = "some.key") + private String someKey; + + @Inject + @ConfigProperty(name = "another.key") + private String anotherKey; + + @Inject + @ConfigProperty(name = "second-key") + private String anotherValue; + + @Test + void testIt() { + assertThat(bean.hello(), is("hello")); + assertThat(value1, is("some.value1")); + assertThat(value2, is("some.value2")); + assertThat(someKey, is("some.value")); + assertThat(anotherKey, is("another.value")); + assertThat(anotherValue, is("test-custom-config-second-value")); + } + + @AddBean(MyBean.class) + @AddConfigBlock(""" + some.key1=some.value1 + some.key2=some.value2 + """) + @AddConfig(key = "second-key", value = "test-custom-config-second-value") + @Configuration(configSources = {"testConfigSources.properties", "testConfigSources.yaml"}) + @Retention(RetentionPolicy.RUNTIME) + static @interface MetaAnnotation { + } + + static class MyBean { + + String hello() { + return "hello"; + } + } +} diff --git a/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestMetaAnnotation.java b/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestMetaAnnotation.java new file mode 100644 index 00000000000..85b6d8c4d31 --- /dev/null +++ b/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestMetaAnnotation.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.tests.testing.testng; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +import jakarta.inject.Inject; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import io.helidon.microprofile.testing.testng.AddBean; +import io.helidon.microprofile.testing.testng.AddConfig; +import io.helidon.microprofile.testing.testng.AddConfigBlock; +import io.helidon.microprofile.testing.testng.Configuration; +import io.helidon.microprofile.testing.testng.HelidonTest; + +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.testng.annotations.Test; + +@TestMetaAnnotation.MetaAnnotation +// @HelidonTest is still mandatory in the test and it has no effect in the MetaAnnotation +@HelidonTest +class TestMetaAnnotation { + + @Inject + MyBean bean; + + @Inject + @ConfigProperty(name = "some.key1") + private String value1; + + @Inject + @ConfigProperty(name = "some.key2") + private String value2; + + @Inject + @ConfigProperty(name = "some.key") + private String someKey; + + @Inject + @ConfigProperty(name = "another.key") + private String anotherKey; + + @Inject + @ConfigProperty(name = "second-key") + private String anotherValue; + + @Test + void testIt() { + assertThat(bean.hello(), is("hello")); + assertThat(value1, is("some.value1")); + assertThat(value2, is("some.value2")); + assertThat(someKey, is("some.value")); + assertThat(anotherKey, is("another.value")); + assertThat(anotherValue, is("test-custom-config-second-value")); + } + + @AddBean(MyBean.class) + @AddConfigBlock(""" + some.key1=some.value1 + some.key2=some.value2 + """) + @AddConfig(key = "second-key", value = "test-custom-config-second-value") + @Configuration(configSources = {"testConfigSources.properties", "testConfigSources.yaml"}) + @Retention(RetentionPolicy.RUNTIME) + static @interface MetaAnnotation { + } + + static class MyBean { + + String hello() { + return "hello"; + } + } +}