diff --git a/bom/pom.xml b/bom/pom.xml index cfd9a3835f8..b45b5a97775 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -1103,6 +1103,11 @@ helidon-common-testing-virtual-threads ${helidon.version} + + io.helidon.microprofile.testing + helidon-microprofile-testing + ${helidon.version} + io.helidon.microprofile.testing helidon-microprofile-testing-junit5 diff --git a/common/testing/virtual-threads/src/main/java/module-info.java b/common/testing/virtual-threads/src/main/java/module-info.java index 246c15c45b2..aa49cc8532a 100644 --- a/common/testing/virtual-threads/src/main/java/module-info.java +++ b/common/testing/virtual-threads/src/main/java/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 Oracle and/or its affiliates. + * Copyright (c) 2024, 2025 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. @@ -20,7 +20,8 @@ module io.helidon.common.testing.vitualthreads { requires jdk.jfr; exports io.helidon.common.testing.virtualthreads to + io.helidon.microprofile.testing, io.helidon.microprofile.testing.junit5, io.helidon.microprofile.testing.testng, io.helidon.webserver.testing.junit5; -} \ No newline at end of file +} diff --git a/docs/pom.xml b/docs/pom.xml index a36517fd1d7..3768bc13a44 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -101,6 +101,16 @@ helidon-microprofile-testing-junit5 true + + io.helidon.microprofile.testing + helidon-microprofile-testing-testng + true + + + org.testng + testng + true + diff --git a/docs/src/main/asciidoc/includes/attributes.adoc b/docs/src/main/asciidoc/includes/attributes.adoc index 1921e8a3392..69457170ddf 100644 --- a/docs/src/main/asciidoc/includes/attributes.adoc +++ b/docs/src/main/asciidoc/includes/attributes.adoc @@ -215,10 +215,14 @@ endif::[] :metrics-serviceapi-javadoc-base-url: {javadoc-base-url}/io.helidon.metrics.serviceapi :micrometer-javadoc-base-url: {javadoc-base-url}/io.helidon.integrations.micrometer :prometheus-javadoc-base-url: {javadoc-base-url}/io.helidon.metrics.prometheus +:mp-config-javadoc-base-url: {javadoc-base-url}/io.helidon.microprofile.config :mp-cors-javadoc-base-url: {javadoc-base-url}/io.helidon.microprofile.cors +:mp-restclient-javadoc-base-url: {javadoc-base-url}/io.helidon.microprofile.restclient :mp-server-javadoc-base-url: {javadoc-base-url}/io.helidon.microprofile.server :mp-tyrus-javadoc-base-url: {javadoc-base-url}/io.helidon.microprofile.tyrus -:mp-restclient-javadoc-base-url: {javadoc-base-url}/io.helidon.microprofile.restclient +:mp-testing-javadoc-base-url: {javadoc-base-url}/io.helidon.microprofile.testing +:mp-junit5-javadoc-base-url: {javadoc-base-url}/io.helidon.microprofile.testing.junit5 +:mp-mocking-javadoc-base-url: {javadoc-base-url}/io.helidon.microprofile.testing.mocking :openapi-javadoc-base-url: {javadoc-base-url}/io.helidon.openapi :openapi-ui-javadoc-base-url: {javadoc-base-url}/io.helidon.integrations.openapi.ui :reactive-base-url: {javadoc-base-url}/io.helidon.common.reactive diff --git a/docs/src/main/asciidoc/mp/guides/testing-junit5.adoc b/docs/src/main/asciidoc/mp/guides/testing-junit5.adoc index 876eef808a0..41496164d2a 100644 --- a/docs/src/main/asciidoc/mp/guides/testing-junit5.adoc +++ b/docs/src/main/asciidoc/mp/guides/testing-junit5.adoc @@ -1,6 +1,6 @@ /////////////////////////////////////////////////////////////////////////////// - Copyright (c) 2021, 2024 Oracle and/or its affiliates. + Copyright (c) 2021, 2025 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. @@ -95,20 +95,27 @@ The test is now complete and verifies the message. The testing extension supports a few additional annotations that allow for finer control of the test execution. - .Optional Extension Annotations -[width="70%",options="header"] +[cols="1,3"] |==================== | Annotation | Description -| `@HelidonTest(resetPerTest = true)` | Resets the container for each method. +| `@HelidonTest(resetPerTest = true)` +| Resets the container for each method. This is useful when we want to modify configuration or beans between executions. In such a case, injection into fields is not possible, as we would need a different instance for each test. -| `@AddConfig(key = "app.greeting", value = "Unite")` | Define additional configuration (either on class level, or method level) by adding a single configuration key/value. + +| `@AddConfig(key = "app.greeting", value = "Unite")` +| Define additional configuration (either on class level, or method level) by adding a single configuration key/value. + | `@AddConfigBlock(type = "properties", value = """ + some.key1=some.value1 + some.key2=some.value2 + -""")` | Define additional configuration (either on class level, or method level) by adding one or more configuration key/value pairs. -| `@Configuration(configSources = "test-config.properties")` | Adds a whole config source from classpath. +""")` +| Define additional configuration (either on class level, or method level) by adding one or more configuration key/value pairs. + +| `@Configuration(configSources = "test-config.properties")` +| Adds a whole config source from classpath. + |==================== Here's an example showing how these approaches are used to execute the same endpoint with different configuration: diff --git a/docs/src/main/asciidoc/mp/testing/testing-common.adoc b/docs/src/main/asciidoc/mp/testing/testing-common.adoc new file mode 100644 index 00000000000..633d2f683f0 --- /dev/null +++ b/docs/src/main/asciidoc/mp/testing/testing-common.adoc @@ -0,0 +1,393 @@ +/////////////////////////////////////////////////////////////////////////////// + + Copyright (c) 2025 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. + +/////////////////////////////////////////////////////////////////////////////// + += Testing with Helidon MP +:rootdir: {docdir}/../.. + +include::{rootdir}/includes/mp.adoc[] + +:mp-testing-javadoc-url: {mp-testing-javadoc-base-url}/io/helidon/microprofile/testing +:mp-mocking-javadoc-url: {mp-mocking-javadoc-base-url}/io/helidon/microprofile/testing/mocking +:mp-server-javadoc-url: {mp-server-javadoc-base-url}/io/helidon/microprofile/server +:mp-config-javadoc-url: {mp-config-javadoc-base-url}/io/helidon/microprofile/config +:jakarta-jaxrs-javadoc-url: {jakarta-jaxrs-base-url}/apidocs/jakarta/ws/rs +:jakarta-cdi-javadoc-url: {jakarta-cdi-base-url}/apidocs/jakarta.cdi/jakarta/enterprise +:microprofile-config-javadoc-url: {microprofile-config-base-url}/apidocs/org/eclipse/microprofile/config + +== Usage + +// tag::usage[] +[source,java] +.Basic usage +---- +include::{sourcedir}/mp/testing/TestingSnippets.java[tag=snippet_1, indent=0] +---- +<1> Enable the test class + +=== CDI Container Setup + +By default, CDI discovery is enabled: + +- CDI beans and extensions in the classpath are added automatically +- If disabled, the CDI beans and extensions must be added manually + +[NOTE] +==== +Customization of the CDI container on a test method changes the CDI container affinity. + +I.e. The test method will use a dedicated CDI container. +==== + +[NOTE] +==== +It is not recommended to provide a `beans.xml` along the test classes, as it would combine beans from all tests. + +Instead, you should use link:{mp-testing-javadoc-url}/AddBean.html[`@AddBean`] to specify the beans per test or method. +==== + +CDI discovery can be disabled using link:{mp-testing-javadoc-url}/DisableDiscovery.html[`@DisableDiscovery`]. + +[source,java] +.Disable discovery +---- +include::{sourcedir}/mp/testing/TestingSnippets.java[tag=snippet_2, indent=0] +---- +<1> Disable CDI discovery +<2> Add a bean class + +When disabling discovery, it can be difficult to identify the CDI extensions needed to activate the desired features. + +JAXRS (Jersey) support can be added easily using link:{mp-testing-javadoc-url}/AddJaxRs.html[`@AddJaxRs`]. + +[source,java] +.Add JAX-RS (Jersey) +---- +include::{sourcedir}/mp/testing/TestingSnippets.java[tag=snippet_3, indent=0] +---- +<1> Add JAX-RS (Jersey) support +<2> Add a resource class to the CDI container + +Note the following Helidon CDI extensions: + +[cols="1,3"] +|=== +|Extension | Note + +|link:{mp-config-javadoc-url}/ConfigCdiExtension.html[`ConfigCdiExtension`] +|Add MicroProfile Config injection support + +|link:{mp-server-javadoc-url}/ServerCdiExtension.html[`ServerCdiExtension`] +|Optional if using link:{mp-testing-javadoc-url}/AddJaxRs.html[`@AddJaxRs`] + +|link:{mp-server-javadoc-url}/JaxRsCdiExtension.html[`JaxRsCdiExtension`] +|Optional if using link:{mp-testing-javadoc-url}/AddJaxRs.html[`@AddJaxRs`] + +|=== + +=== CDI Container Afinity + +By default, one CDI container is created per test class and is shared by all test methods. + +However, test methods can also require a dedicated CDI container: + +- By forcing a reset of the CDI container between methods +- By customizing the CDI container per test method + +[source,java] +.Reset the CDI container between methods +---- +include::{sourcedir}/mp/testing/TestingSnippets.java[tag=snippet_4, indent=0] +---- +<1> `testOne` executes in a dedicated CDI container +<2> `testTwo` also executes in a dedicated CDI container + +[source,java] +.Customize the CDI container per method +---- +include::{sourcedir}/mp/testing/TestingSnippets.java[tag=snippet_5, indent=0] +---- +<1> `testOne` executes in the shared CDI container +<2> `testTwo` executes in a dedicated CDI container + +=== Configuration + +The test configuration can be set up in two exclusive ways: + +- Using the "synthetic" configuration expressed with annotations (default) +- Using the "existing" configuration of the current environment + +Use link:{mp-testing-javadoc-url}/Configuration.html[`@Configuration`] to switch to the "existing" configuration. + +[source,java] +.Switch to the existing configuration +---- +include::{sourcedir}/mp/testing/TestingSnippets.java[tag=snippet_6, indent=0] +---- + +[NOTE] +==== +Customization of the test configuration on a test method changes the CDI container affinity. + +I.e. The test method will use a dedicated CDI container. +==== + +==== Synthetic Configuration + +The "synthetic" configuration can be expressed using the following annotations: + +[cols="1,3"] +|=== +|Type | Usage + +|link:{mp-testing-javadoc-url}/AddConfig.html[`@AddConfig`] +|Key value pair + +|link:{mp-testing-javadoc-url}/AddConfigBlock.html[`@AddConfigBlock`] +|Formatted text block + +|link:{mp-testing-javadoc-url}/Configuration.html[`@Configuration`] +| Classpath resources using + +|=== + +[source,java] +.Add a key value pair +---- +include::{sourcedir}/mp/testing/TestingSnippets.java[tag=snippet_7, indent=0] +---- + +[source,java] +.Add a properties text block +---- +include::{sourcedir}/mp/testing/TestingSnippets.java[tag=snippet_8, indent=0] +---- + +[source,java] +.Add a YAML text block +---- +include::{sourcedir}/mp/testing/TestingSnippets.java[tag=snippet_9, indent=0] +---- + +[source,java] +.Add classpath resources +---- +include::{sourcedir}/mp/testing/TestingSnippets.java[tag=snippet_10, indent=0] +---- + +==== Configuration Ordering + +The ordering of the test configuration can be controlled using the mechanism defined by the +link:{microprofile-config-spec-url}#_configsource_ordering[MicroProfile Config specification]. + +NOTE: The configuration expressed with link:{mp-testing-javadoc-url}/AddConfig.html[`@AddConfig`] has a fixed ordinal value +of `1000` + +[source,java] +.Add a properties text block with ordinal +---- +include::{sourcedir}/mp/testing/TestingSnippets.java[tag=snippet_11, indent=0] +---- + +=== Injectable Types + +Helidon provides injection support for types that reflect the current server. E.g. JAXRS client. + +Here are all the built-in types that can be injected: +[cols="1,3"] +|=== +|Type | Usage + +|link:{jakarta-jaxrs-javadoc-url}/client/WebTarget.html[`WebTarget`] +|A JAX-RS client configured for the current server. + +|`URI` +|A URI representing the current server + +|`String` +|A raw URI representing the current server + +|link:{jakarta-cdi-javadoc-url}/inject/se/SeContainer.html[`SeContainer`] +| The current CDI container instance + +|=== + +NOTE: Types that reflect the current server require link:{mp-server-javadoc-url}/ServerCdiExtension.html[`ServerCdiExtension`] + +NOTE: All the injectable types are also available as method parameters. + +[source,java] +.Inject a JAX-RS client for the default socket +---- +include::{sourcedir}/mp/testing/TestingSnippets.java[tag=snippet_12, indent=0] +---- + +Use link:{mp-testing-javadoc-url}/Socket.html[`@Socket`] to specify the socket for the clients and URIs. + +[source,java] +.Inject a JAX-RS client for the admin socket +---- +include::{sourcedir}/mp/testing/TestingSnippets.java[tag=snippet_13, indent=0] +---- + +[source,java] +.Using a method parameter +---- +include::{sourcedir}/mp/testing/TestingSnippets.java[tag=snippet_14, indent=0] +---- +// end::usage[] + +== API + +// tag::api[] +Here is a brief overview of the MicroProfile testing annotations: + +[cols="1,3"] +|=== +|Annotation | Usage + +|link:{mp-testing-javadoc-url}/AddBean.html[`@AddBean`] +|Add a CDI bean class to the CDI container + +|link:{mp-testing-javadoc-url}/AddExtension.html[`@AddExtension`] +|Add a CDI extension to the CDI container + +|link:{mp-testing-javadoc-url}/DisableDiscovery.html[`@DisableDiscovery`] +|Disable automated discovery of beans and extensions + +|link:{mp-testing-javadoc-url}/AddJaxRs.html[`@AddJaxRs`] +|Shorthand to add JAX-RS (Jersey) support + +|link:{mp-testing-javadoc-url}/AddConfig.html[`@AddConfig`] +|Define a key value pair in the "synthetic" configuration + +|link:{mp-testing-javadoc-url}/AddConfigBlock.html[`@AddConfigBlock`] +|Define a formatted text block in the "synthetic" configuration + +|link:{mp-testing-javadoc-url}/Configuration.html[`@Configuration`] +| Switch between "synthetic" and "existing" ; Add classpath resources to the "synthetic" configuration + +|link:{mp-testing-javadoc-url}/Socket.html[`@Socket`] +| CDI qualifier to inject a JAX-RS client or URI for a named socket + +|link:{mp-testing-javadoc-url}/AfterStop.html[`@AfterStop`] +| Mark a static method to be executed after the container is stopped + +|=== +// end::api[] + +== Examples + +// tag::examples[] +=== Config Injection Example + +The following example demonstrates how to enable the use of + link:{microprofile-config-javadoc-url}/inject/ConfigProperty.html[`@ConfigProperty`] without CDI discovery. + +[source,java] +.Config Injection Example +---- +include::{sourcedir}/mp/testing/TestingSnippets.java[tag=snippet_15, indent=0] +---- +<1> CDI discovery is disabled +<2> Add `MyBean` to the CDI container +<3> Add link:{mp-config-javadoc-url}/ConfigCdiExtension.html[`ConfigCdiExtension`] to the CDI container +<4> Define test configuration +<5> Inject the configuration + +=== Request Scope Example + +The following example demonstrates how to use link:{jakarta-cdi-javadoc-url}/context/RequestScoped.html[`@RequestScoped`] with +JAXRS without CDI discovery. + +[source,java] +.Request Scope Example +---- +include::{sourcedir}/mp/testing/TestingSnippets.java[tag=snippet_16, indent=0] +---- +<1> CDI discovery is disabled +<2> Add JAXRS (Jersey) support +<3> Add `MyResource` to the CDI container +// end::examples[] + +== Mock Support + +// tag::mock-support[] +Mocking in Helidon MP is all about replacing CDI beans with instrumented mock classes. + +This can be done using CDI alternatives, however Helidon provides an annotation to make it easy. + +=== Maven Coordinates + +To enable mock mupport add the following dependency to your project’s pom.xml. +[source,xml] +---- + + io.helidon.microprofile.testing + helidon-microprofile-testing-mocking + test + +---- + +=== Usage + +Use the link:{mp-mocking-javadoc-url}/MockBean.html[`@MockBean`] annotation to inject an instrumented CDI bean in your test, + and customize it in the test method. + +==== Example + +[source,java] +.Mocking using `@MockBean` +---- +include::{sourcedir}/mp/testing/TestingSnippets.java[tag=snippet_17, indent=0] +---- +<1> Instrument `MyService` using `Answers.CALLS_REAL_METHODS` +<2> Customize the behavior +// end::mock-support[] + +== Virtual Threads + +// tag::virtual-threads[] +Virtual Threads pinning can be detected during tests. + +A virtual thread is "pinning" when it blocks its carrier thread in a way that prevents the virtual thread scheduler from + scheduling other virtual threads. + +This can happen when blocking in native code, or prior to JDK24 when a blocking IO operation happens in a synchronized block. + +Pinning can in some cases negatively affect application performance. + +[source,java] +.Enable pinning detection +---- +include::{sourcedir}/mp/testing/TestingSnippets.java[tag=snippet_18, indent=0] +---- + +Pinning is considered harmful when it takes longer than 20 milliseconds, +that is also the default when detecting it within tests. + +Pinning threshold can be changed with: + +[source,java] +.Configure pinning threshold +---- +include::{sourcedir}/mp/testing/TestingSnippets.java[tag=snippet_19, indent=0] +---- +<1> Change pinning threshold from default(20) to 50 milliseconds. + +When pinning is detected, the test fails with a stacktrace pointing at the culprit. +// end::virtual-threads[] diff --git a/docs/src/main/asciidoc/mp/testing/testing-ng.adoc b/docs/src/main/asciidoc/mp/testing/testing-ng.adoc index 62ddbd62fd3..dcd88cb8f8c 100644 --- a/docs/src/main/asciidoc/mp/testing/testing-ng.adoc +++ b/docs/src/main/asciidoc/mp/testing/testing-ng.adoc @@ -1,6 +1,6 @@ /////////////////////////////////////////////////////////////////////////////// - Copyright (c) 2022, 2024 Oracle and/or its affiliates. + Copyright (c) 2022, 2025 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. @@ -16,30 +16,38 @@ /////////////////////////////////////////////////////////////////////////////// -= Testing with Test NG += Testing with TestNG :description: Helidon Testing with TestNG :keywords: helidon, mp, test, testing, testng :feature-name: Testing with TestNG :rootdir: {docdir}/../.. - include::{rootdir}/includes/mp.adoc[] +:mp-testing-javadoc-url: {mp-testing-javadoc-base-url}/io/helidon/microprofile/testing +:mp-mocking-javadoc-url: {mp-mocking-javadoc-base-url}/io/helidon/microprofile/testing/mocking +:mp-server-javadoc-url: {mp-server-javadoc-base-url}/io/helidon/microprofile/server +:mp-config-javadoc-url: {mp-config-javadoc-base-url}/io/helidon/microprofile/config +:jakarta-jaxrs-javadoc-url: {jakarta-jaxrs-base-url}/apidocs/jakarta/ws/rs +:jakarta-cdi-javadoc-url: {jakarta-cdi-base-url}/apidocs/jakarta.cdi/jakarta/enterprise +:microprofile-config-javadoc-url: {microprofile-config-base-url}/apidocs/org/eclipse/microprofile/config + == Contents - <> - <> - <> -- <> - <> +- <> - <> - <> == Overview -Helidon provides built-in test support for CDI testing in TestNG. +Helidon provides a TestNG listener that integrates CDI to support testing with Helidon MP. -include::{rootdir}/includes/dependencies.adoc[] +The test class is added as a CDI bean to support injection and the CDI container is started lazily during test execution. +include::{rootdir}/includes/dependencies.adoc[] [source,xml] ---- @@ -49,135 +57,48 @@ include::{rootdir}/includes/dependencies.adoc[] ---- -== Usage - default -A test can be annotated with `io.helidon.microprofile.testing.testng.HelidonTest` annotation to mark it as a -CDI test. This annotation will start the CDI container before any test method is invoked, and stop it after -the last method is invoked. This annotation also enables injection into the test class itself. - == Usage -A test can be annotated with `io.helidon.microprofile.testing.testng.HelidonTest` annotation to mark it as a -CDI test. This annotation will start the CDI container before any test method is invoked, and stop it after -the last method is invoked. This annotation also enables injection into the test class itself. - -=== Usage - per method CDI container -A test can be annotated as follows: - -`@HelidonTest(resetPerTest = true)` - -This will change the behavior as follows: +include::{rootdir}/mp/testing/testing-common.adoc[tag=usage] -- A new CDI container is created for each test method invocation -- annotations to add config, beans and extension can be added for each method in addition to the class -- you cannot inject fields or constructor parameters of the test class itself (as a single instance is shared by more containers) +=== Test Instance Lifecyle -=== Usage - configuration -In addition to the `@AddConfig` and `@AddConfigBlock` annotations, you can also use -`@Configuration` to configure additional classpath properties config sources using `configSources`, and to -mark that a custom configuration is desired. -If `@Configuration(useExisting=true)`, the existing (or default) MicroProfile configuration would be used. In this case -it is important to set property `mp.initializer.allow=true` in order CDI container to start, when used with -`@HelidonTest`. -You can set up config in `@BeforeAll` method and register it with `ConfigProviderResolver` using MP Config APIs, and declare -`@Configuration(useExisting=true)`. -Note that this is not compatible with repeatable tests that use method sources that access CDI, as we must delay the CDI -startup to the test class instantiation (which is too late, as the method sources are already invoked by this time). +The test instance lifecycle is a pseudo singleton that follows the lifecycle of the CDI container. -*If you want to use method sources that use CDI with repeatable tests, please do not use `@Configuration(useExisting=true)`* +I.e. By default, the test instance is re-used between test methods. -NOTE: Test method parameters are currently not supported. +NOTE: The test instance is not re-used between CDI container, using a dedicated CDI container implies a new test instance == API -The annotations described in this section are inherited (for the non-repeatable ones), and additive (for repeatable). -So if you declare `@DisableDiscovery` on abstract class, all implementations will have discovery disabled, unless you -annotate the implementation class with `@DisableDiscovery(false)`. -If you declare `@AddBean` on both abstract class and implementation class, both beans will be added. +include::{rootdir}/mp/testing/testing-common.adoc[tag=api] -In addition to this simplification, the following annotations are supported: - - -|=== -|Annotation | Usage - -|`@io.helidon.microprofile.testing.testng.AddBean` -|Used to add one or more beans to the container (if not part of a bean archive, or when discovery is disabled) - -|`@io.helidon.microprofile.testing.testng.AddExtension` -|Used to add one or more CDI extensions to the container (if not added through service loader, or when discovery is disabled) - -|`@io.helidon.microprofile.testing.testng.AddConfig` -|Used to add one or more configuration properties to MicroProfile config without the need of creating a `microprofile-config.properties` file +== Examples -|`@io.helidon.microprofile.testing.testng.DisableDiscovery` -|Used to disable automated discovery of beans and extensions +include::{rootdir}/mp/testing/testing-common.adoc[tag=examples] -|Used `@io.helidon.microprofile.testing.junit5.AddJaxRs` -a|add JaxRs support to the test class. Only used with `@DisableDiscovery` annotation, otherwise an exception will be thrown. Automatically adds the following Beans and Extensions to the test class: +== Mock Support -* `ServerCdiExtension` -* `JaxRsCdiExtension` -* `CdiComponentProvider` -* `org.glassfish.jersey.ext.cdi1x.internal.ProcessAllAnnotatedTypes` -* `org.glassfish.jersey.weld.se.WeldRequestScope` -|=== +include::{rootdir}/mp/testing/testing-common.adoc[tag=mock-support] -== Examples +=== Using CDI Alternative -In the current example, Helidon container will be launched prior test. The _Bean Discovery_ will be disabled. _MyBean_ will be added to the test, so that it can be injected. _ConfigCdiExtension_ will be enabled for this test. And finally, a configuration property will be added using `@AddConfig` annotation. +link:{jakarta-cdi-javadoc-url}/inject/Alternative.html[`@Alternative`] can be used to replace a CDI bean with an instrumented +instance. [source,java] -.Code sample +.Mocking using CDI Alternative ---- include::{sourcedir}/mp/testing/TestingNgSnippets.java[tag=snippet_1, indent=0] ---- -<1> Start the Helidon container. -<2> Set disabled Bean Discovery for the current test class. -<3> Add `MyBean` to current context. -<4> Add a configuration CDI extension to the current test. -<5> Add configuration properties. -<6> Inject `MyBean` as it is available in the CDI context. -<7> Run rests. - -To test `@RequestScoped` bean with JaxRs support: - -[source,java] -.Test `RequestScoped` bean ----- -include::{sourcedir}/mp/testing/TestingNgSnippets.java[tag=snippet_2, indent=0] ----- -<1> Start the Helidon container. -<2> Set disabled Bean discovery. -<3> Add JaxRs support to the current test class. -<4> Define a `RequestScoped` bean. - +<1> Create the mock instance in the test class +<2> Create a CDI producer method annotated with `@Alternative` +<3> Set priority to 1 (required by `@Alternative`) +<4> Customize the behavior == Virtual Threads -Helidon tests are able to detect Virtual Threads pinning. A situation when carrier thread is blocked -in a way, that virtual thread scheduler can't use it for scheduling of other virtual threads. -This can happen for example when blocking native code is invoked, or prior to the JDK 24 when -blocking IO operation happens in a synchronized block. -Pinning can in some cases negatively affect application performance. - -[source,java] -.Enable pinning detection ----- -include::{sourcedir}/mp/testing/TestingNgSnippets.java[tag=snippet_3, indent=0] ----- - -Pinning is considered as harmful when it takes longer than 20 milliseconds, -that is also the default when detecting it within Helidon tests. - -Pinning threshold can be changed with: - -[source,java] -.Configure pinning threshold ----- -include::{sourcedir}/mp/testing/TestingNgSnippets.java[tag=snippet_4, indent=0] ----- -<1> Change pinning threshold from default(20) to 50 milliseconds. -When pinning is detected, test fails with stacktrace pointing to the line of code causing it. +include::{rootdir}/mp/testing/testing-common.adoc[tag=virtual-threads] == Reference diff --git a/docs/src/main/asciidoc/mp/testing/testing.adoc b/docs/src/main/asciidoc/mp/testing/testing.adoc index fab51290a0d..bf6a5724369 100644 --- a/docs/src/main/asciidoc/mp/testing/testing.adoc +++ b/docs/src/main/asciidoc/mp/testing/testing.adoc @@ -1,6 +1,6 @@ /////////////////////////////////////////////////////////////////////////////// - Copyright (c) 2020, 2024 Oracle and/or its affiliates. + Copyright (c) 2020, 2025 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. @@ -21,11 +21,20 @@ :pagename: testing :description: Helidon Testing with JUnit5 :keywords: helidon, mp, test, testing, junit -:feature-name: Testing with JUnit +:feature-name: Helidon MicroProfile Testing JUnit5 :rootdir: {docdir}/../.. include::{rootdir}/includes/mp.adoc[] +:mp-junit5-javadoc-url: {mp-junit5-javadoc-base-url}/io/helidon/microprofile/testing/junit5 +:mp-testing-javadoc-url: {mp-testing-javadoc-base-url}/io/helidon/microprofile/testing +:mp-mocking-javadoc-url: {mp-mocking-javadoc-base-url}/io/helidon/microprofile/testing/mocking +:mp-server-javadoc-url: {mp-server-javadoc-base-url}/io/helidon/microprofile/server +:mp-config-javadoc-url: {mp-config-javadoc-base-url}/io/helidon/microprofile/config +:jakarta-jaxrs-javadoc-url: {jakarta-jaxrs-base-url}/apidocs/jakarta/ws/rs +:jakarta-cdi-javadoc-url: {jakarta-cdi-base-url}/apidocs/jakarta.cdi/jakarta/enterprise +:microprofile-config-javadoc-url: {microprofile-config-base-url}/apidocs/org/eclipse/microprofile/config + == Contents - <> @@ -39,7 +48,9 @@ include::{rootdir}/includes/mp.adoc[] == Overview -Helidon provides built-in test support for CDI testing in JUnit5. +Helidon provides a JUnit5 extension that integrates CDI to support testing with Helidon MP. + +The test class is added as a CDI bean to support injection and the CDI container is started lazily during test execution. include::{rootdir}/includes/dependencies.adoc[] [source,xml] @@ -52,205 +63,54 @@ include::{rootdir}/includes/dependencies.adoc[] ---- == Usage -A test can be annotated with `io.helidon.microprofile.testing.junit5.HelidonTest` annotation to mark it as a -CDI test. This annotation will start the CDI container before any test method is invoked, and stop it after -the last method is invoked. This annotation also enables injection into the test class itself. - -=== Usage - per method CDI container -A test can be annotated as follows: - -`@HelidonTest(resetPerTest = true)` - -This will change the behavior as follows: - -- A new CDI container is created for each test method invocation -- annotations to add config, beans and extension can be added for each method in addition to the class -- you cannot inject fields or constructor parameters of the test class itself (as a single instance is shared by more containers) -- you can add `SeContainer` as a method parameter of any test method and you will get the current container - -=== Usage - configuration -In addition to the `@AddConfig` and `@AddConfigBlock` annotations, you can also use -`@Configuration` to configure additional classpath properties config sources using `configSources`, and to -mark that a custom configuration is desired. -If `@Configuration(useExisting=true)`, the existing (or default) MicroProfile configuration would be used. In this case -it is important to set property `mp.initializer.allow=true` in order CDI container to start, when used with -`@HelidonTest`. -You can set up config in `@BeforeAll` method and register it with `ConfigProviderResolver` using MP Config APIs, and declare -`@Configuration(useExisting=true)`. -Note that this is not compatible with repeatable tests that use method sources that access CDI, as we must delay the CDI -startup to the test class instantiation (which is too late, as the method sources are already invoked by this time). - -*If you want to use method sources that use CDI with repeatable tests, please do not use `@Configuration(useExisting=true)`* - -=== Usage - added parameters and injection types -The following types are available for injection (when a single CDI container is used per test class): - -- `WebTarget` - a JAX-RS client's target configured for the current hostname and port when `helidon-micorprofile-server` is on -the classpath - -The following types are available as method parameters (in any type of Helidon tests): - -- `WebTarget` - a JAX-RS client's target configured for the current hostname and port when `helidon-micorprofile-server` is on -the classpath -- `SeContainer` - the current container instance - -== API - -The annotations described in this section are inherited (for the non-repeatable ones), and additive (for repeatable). -So if you declare `@DisableDiscovery` on abstract class, all implementations will have discovery disabled, unless you -annotate the implementation class with `@DisableDiscovery(false)`. -If you declare `@AddBean` on both abstract class and implementation class, both beans will be added. -In addition to this simplification, the following annotations are supported: +include::{rootdir}/mp/testing/testing-common.adoc[tag=usage] -|=== -|Annotation | Usage +=== Test Instance Lifecyle -|`@io.helidon.microprofile.testing.junit5.AddBean` -|Used to add one or more beans to the container (if not part of a bean archive, or when discovery is disabled) +The CDI scope used by the test instance follows the lifecyle defined by JUnit5. The default is `PER_CLASS` and is enforced + by link:{mp-junit5-javadoc-url}/HelidonTest.html[`@HelidonTest`]. -|`@io.helidon.microprofile.testing.junit5.AddExtension` -|Used to add one or more CDI extensions to the container (if not added through service loader, or when discovery is disabled) +I.e. By default, the test instance is re-used between test methods. -|`@io.helidon.microprofile.testing.junit5.AddConfig` -|Used to add one or more configuration properties to MicroProfile config without the need of creating a `microprofile-config.properties` file - -|Used `@io.helidon.microprofile.testing.junit5.DisableDiscovery` -|to disable automated discovery of beans and extensions - -|Used `@io.helidon.microprofile.testing.junit5.DisableDiscovery` -|to disable automated discovery of beans and extensions - -|Used `@io.helidon.microprofile.testing.junit5.AddJaxRs` -a|add JaxRs support to the test class. Only used with `@DisableDiscovery` annotation, otherwise an exception will be thrown. Automatically adds the following Beans and Extensions to the test class: - -* `ServerCdiExtension` -* `JaxRsCdiExtension` -* `CdiComponentProvider` -* `org.glassfish.jersey.ext.cdi1x.internal.ProcessAllAnnotatedTypes` -* `org.glassfish.jersey.weld.se.WeldRequestScope` -|=== - -== Examples - -In the current example, Helidon container will be launched prior test. The _Bean Discovery_ will be disabled. _MyBean_ will be added to the test, so that it can be injected. _ConfigCdiExtension_ will be enabled for this test. And finally, a configuration property will be added using `@AddConfig` annotation. +NOTE: The test instance is not re-used between CDI container, using a dedicated CDI container implies a new test instance [source,java] -.Code sample ----- -include::{sourcedir}/mp/testing/TestingSnippets.java[tag=snippet_1, indent=0] ----- -<1> Start the Helidon container. -<2> Set disabled Bean Discovery for the current test class. -<3> Add `MyBean` to current context. -<4> Add a configuration CDI extension to the current test. -<5> Add configuration properties. -<6> Inject `MyBean` as it is available in the CDI context. -<7> Run rests. - -To test `@RequestScoped` bean with JaxRs support: - -[source,java] -.Test `RequestScoped` bean ----- -include::{sourcedir}/mp/testing/TestingSnippets.java[tag=snippet_2, indent=0] ----- -<1> Start the Helidon container. -<2> Set disabled Bean discovery. -<3> Add JaxRs support to the current test class. -<4> Define a `RequestScoped` bean. - -== Mock Support - -This section describes how to mock objects using Helidon API and in a second phase, using pure CDI. - -=== Helidon Mock Support - -Helidon has its own API to use mocking with test classes annotated with `@HelidonTest`. - -==== Maven Coordinates - -To enable Helidon Mock Support add the following dependency to your project’s pom.xml. -[source,xml] +.Using per method lifecycle ---- - - io.helidon.microprofile.testing - helidon-microprofile-testing-mocking - test - +include::{sourcedir}/mp/testing/TestingJunit5Snippets.java[tag=snippet_1, indent=0] ---- -==== API +== API -It consists of one annotation named `@MockBean`, designed to be used on fields and parameters. The implementation -relies only on CDI and thus it works with either JUnit or TestNG. The annotation has a parameter `answers` used -to set the default answer for the mocked beans. +include::{rootdir}/mp/testing/testing-common.adoc[tag=api] -==== Example +== Examples -[source,java] -.Code sample ----- -include::{sourcedir}/mp/testing/HelidonMockingSnippets.java[tag=snippet_1, indent=0] ----- -<1> `service` field annotated with `@MockBean` and `Answers.CALLS_REAL_METHODS` for default answers. -<2> Test the mocked service with real method response. -<3> Test the mocked service with modified behavior. +include::{rootdir}/mp/testing/testing-common.adoc[tag=examples] -=== Mocking objects with pure CDI +== Mock Support -CDI can be used to enable mocking, the following example shows how to mock a service and inject it in a JAX-RS resource. -Let's consider a simple service `FooService` with a dummy method `FooService#getFoo` that return `foo` as a `String`. -And a resource where the service is injected and expose an endpoint at `/foo` that get the `String` provided by the service. +include::{rootdir}/mp/testing/testing-common.adoc[tag=mock-support] -[source,java] -.Code sample ----- -include::{sourcedir}/mp/testing/CDIMockingSnippets.java[tag=snippet_1, indent=0] ----- -<1> A simple `foo` Service. -<2> Inject the service into the resource. +=== Using CDI Alternative -This example uses Mockito to create mock instances before each test. The method mockFooService is a CDI producer method -that adds the mock instance as an @Alternative in order to replace the FooService singleton. +link:{jakarta-cdi-javadoc-url}/inject/Alternative.html[`@Alternative`] can be used to replace a CDI bean with an instrumented +instance. [source,java] -.Code sample +.Mocking using CDI Alternative ---- -include::{sourcedir}/mp/testing/CDIMockingSnippets.java[tag=snippet_2, indent=0] +include::{sourcedir}/mp/testing/TestingJunit5Snippets.java[tag=snippet_2, indent=0] ---- -<1> Set priority to 1 because of `@Alternative`. -<2> Set `fooService` to a new mock before each tests. -<3> This makes `FooResource` inject the mock instead of the default singleton. -<4> Test that the mock is injected with modified behavior. -<5> Test the real method behavior. +<1> Create the mock instance in the test class +<2> Create a CDI producer method annotated with `@Alternative` +<3> Set priority to 1 (required by `@Alternative`) +<4> Customize the behavior == Virtual Threads -Helidon tests are able to detect Virtual Threads pinning. A situation when carrier thread is blocked -in a way, that virtual thread scheduler can't use it for scheduling of other virtual threads. -This can happen for example when blocking native code is invoked, or prior to the JDK 24 when -blocking IO operation happens in a synchronized block. -Pinning can in some cases negatively affect application performance. - -[source,java] -.Enable pinning detection ----- -include::{sourcedir}/mp/testing/TestingSnippets.java[tag=snippet_3, indent=0] ----- - -Pinning is considered as harmful when it takes longer than 20 milliseconds, -that is also the default when detecting it within Helidon tests. - -Pinning threshold can be changed with: - -[source,java] -.Configure pinning threshold ----- -include::{sourcedir}/mp/testing/TestingSnippets.java[tag=snippet_4, indent=0] ----- -<1> Change pinning threshold from default(20) to 50 milliseconds. -When pinning is detected, test fails with stacktrace pointing to the line of code causing it. +include::{rootdir}/mp/testing/testing-common.adoc[tag=virtual-threads] == Additional Information diff --git a/docs/src/main/java/io/helidon/docs/mp/testing/CDIMockingSnippets.java b/docs/src/main/java/io/helidon/docs/mp/testing/CDIMockingSnippets.java deleted file mode 100644 index a0841f7edc3..00000000000 --- a/docs/src/main/java/io/helidon/docs/mp/testing/CDIMockingSnippets.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * 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.docs.mp.testing; - -import io.helidon.microprofile.testing.junit5.HelidonTest; - -import jakarta.annotation.Priority; -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.enterprise.inject.Alternative; -import jakarta.inject.Inject; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.client.WebTarget; -import jakarta.ws.rs.core.Response; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.Answers; -import org.mockito.Mockito; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; -import static org.mockito.Mockito.when; - -@SuppressWarnings("ALL") -class CDIMockingSnippets { - - // tag::snippet_1[] - @ApplicationScoped - public class FooService { // <1> - - public String getFoo() { - return "foo"; - } - } - - @Path("/foo") - public class FooResource { - - @Inject - private FooService fooService; // <2> - - @GET - public String getFoo() { - return fooService.getFoo(); - } - } - // end::snippet_1[] - - // tag::snippet_2[] - @HelidonTest - @Priority(1) // <1> - class FooTest { - - @Inject - private WebTarget target; - - private FooService fooService; - - @BeforeEach - void initMock() { - fooService = Mockito.mock(FooService.class, Answers.CALLS_REAL_METHODS); // <2> - } - - @Produces - @Alternative - FooService mockFooService() { - return fooService; // <3> - } - - @Test - void testMockedService() { - when(fooService.getFoo()).thenReturn("bar"); // <4> - - Response response = target.path("/foo").request().get(); - - assertThat(response.getStatus(), is(200)); - assertThat(response.readEntity(String.class), is("bar")); - } - - @Test - void testService() { - Response response = target.path("/foo").request().get(); // <5> - - assertThat(response.getStatus(), is(200)); - assertThat(response.readEntity(String.class), is("foo")); - } - } - // end::snippet_2[] -} diff --git a/docs/src/main/java/io/helidon/docs/mp/testing/HelidonMockingSnippets.java b/docs/src/main/java/io/helidon/docs/mp/testing/HelidonMockingSnippets.java deleted file mode 100644 index 0ad2cb5cbbe..00000000000 --- a/docs/src/main/java/io/helidon/docs/mp/testing/HelidonMockingSnippets.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * 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.docs.mp.testing; - -import io.helidon.microprofile.testing.junit5.AddBean; -import io.helidon.microprofile.testing.junit5.HelidonTest; -import io.helidon.microprofile.testing.mocking.MockBean; - -import jakarta.inject.Inject; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.client.WebTarget; - -import org.junit.jupiter.api.Test; -import org.mockito.Answers; -import org.mockito.Mockito; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; - -@SuppressWarnings("ALL") -class HelidonMockingSnippets { - - // tag::snippet_1[] - @HelidonTest - @AddBean(MockBeanAnswerTest.Resource.class) - @AddBean(MockBeanAnswerTest.Service.class) - class MockBeanAnswerTest { - - @MockBean(answer = Answers.CALLS_REAL_METHODS) // <1> - private Service service; - @Inject - private WebTarget target; - - @Test - void injectionTest() { - String response = target.path("/test").request().get(String.class); - assertThat(response, is("Not Mocked")); // <2> - Mockito.when(service.test()).thenReturn("Mocked"); - response = target.path("/test").request().get(String.class); - assertThat(response, is("Mocked")); // <3> - } - - @Path("/test") - public static class Resource { - - @Inject - private Service service; - - @GET - public String test() { - return service.test(); - } - } - - static class Service { - - String test() { - return "Not Mocked"; - } - - } - } - // end::snippet_1[] -} diff --git a/docs/src/main/java/io/helidon/docs/mp/testing/TestingJunit5Snippets.java b/docs/src/main/java/io/helidon/docs/mp/testing/TestingJunit5Snippets.java new file mode 100644 index 00000000000..a90a0dfe4f9 --- /dev/null +++ b/docs/src/main/java/io/helidon/docs/mp/testing/TestingJunit5Snippets.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2024, 2025 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.docs.mp.testing; + +import io.helidon.microprofile.testing.junit5.HelidonTest; + +import jakarta.annotation.Priority; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Alternative; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.Response; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.mockito.Answers; +import org.mockito.Mockito; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +@SuppressWarnings("ALL") +class TestingJunit5Snippets { + + class Snippet1 { + + // tag::snippet_1[] + @TestInstance(TestInstance.Lifecycle.PER_METHOD) + @HelidonTest + class MyTest { + } + // end::snippet_1[] + } + + class Snippet2 { + + // tag::snippet_2[] + @HelidonTest + @Priority(1) // <3> + class MyTest { + + @Inject + WebTarget target; + + MyService myService; + + @BeforeEach + void initMock() { + myService = Mockito.mock(MyService.class, Answers.CALLS_REAL_METHODS); // <1> + } + + @Produces + @Alternative // <2> + MyService mockService() { + return myService; + } + + @Test + void testService() { + Mockito.when(myService.test()).thenReturn("Mocked"); // <4> + Response response = target.path("/test").request().get(); + assertThat(response, is("Mocked")); + } + } + + @Path("/test") + class MyResource { + + @Inject + MyService myService; + + @GET + String test() { + return myService.test(); + } + } + + @ApplicationScoped + class MyService { + + String test() { + return "Not Mocked"; + } + } + // end::snippet_2[] + } +} diff --git a/docs/src/main/java/io/helidon/docs/mp/testing/TestingNgSnippets.java b/docs/src/main/java/io/helidon/docs/mp/testing/TestingNgSnippets.java index 5f07d7c26ab..40cd0a2288b 100644 --- a/docs/src/main/java/io/helidon/docs/mp/testing/TestingNgSnippets.java +++ b/docs/src/main/java/io/helidon/docs/mp/testing/TestingNgSnippets.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 Oracle and/or its affiliates. + * Copyright (c) 2024, 2025 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. @@ -15,92 +15,78 @@ */ package io.helidon.docs.mp.testing; -import io.helidon.microprofile.config.ConfigCdiExtension; -import io.helidon.microprofile.testing.testng.AddBean; -import io.helidon.microprofile.testing.testng.AddConfig; -import io.helidon.microprofile.testing.testng.AddExtension; -import io.helidon.microprofile.testing.testng.AddJaxRs; -import io.helidon.microprofile.testing.testng.DisableDiscovery; import io.helidon.microprofile.testing.testng.HelidonTest; -import jakarta.enterprise.context.RequestScoped; +import jakarta.annotation.Priority; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Alternative; import jakarta.inject.Inject; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; import jakarta.ws.rs.client.WebTarget; import jakarta.ws.rs.core.Response; -import org.junit.jupiter.api.Test; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; +import org.mockito.Answers; +import org.mockito.Mockito; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.notNullValue; @SuppressWarnings("ALL") class TestingNgSnippets { - // stub - static class MyBean { - String greeting() { - return ""; - } - } + class Snippet1 { - // tag::snippet_1[] - @HelidonTest // <1> - @DisableDiscovery // <2> - @AddBean(MyBean.class) // <3> - @AddExtension(ConfigCdiExtension.class) // <4> - @AddConfig(key = "app.greeting", value = "TestHello") // <5> - class TestExample { - @Inject - private MyBean myBean; // <6> - - @Test - void testGreeting() { // <7> - assertThat(myBean, notNullValue()); - assertThat(myBean.greeting(), is("TestHello")); - } - } - // end::snippet_1[] - - // tag::snippet_2[] - @HelidonTest // <1> - @DisableDiscovery // <2> - @AddJaxRs // <3> - @AddBean(TestReqScopeDisabledDiscovery.MyResource.class) - public class TestReqScopeDisabledDiscovery { - - @Inject - private WebTarget target; - - @Test - void testGet() { - String greeting = target.path("/greeting") - .request().get(String.class); - assertThat(greeting, is("Hallo!")); + // tag::snippet_1[] + @HelidonTest + @Priority(1) // <3> + class MyTest { + + @Inject + WebTarget target; + + MyService myService; + + @BeforeMethod + void initMock() { + myService = Mockito.mock(MyService.class, Answers.CALLS_REAL_METHODS); // <1> + } + + @Produces + @Alternative // <2> + MyService mockService() { + return myService; + } + + @Test + void testService() { + Mockito.when(myService.test()).thenReturn("Mocked"); // <4> + Response response = target.path("/test").request().get(); + assertThat(response, is("Mocked")); + } } - @Path("/greeting") - @RequestScoped // <4> - public static class MyResource { + @Path("/test") + class MyResource { + + @Inject + MyService myService; + @GET - public Response get() { - return Response.ok("Hallo!").build(); + String test() { + return myService.test(); } } - } - // end::snippet_2[] - // tag::snippet_3[] - @HelidonTest(pinningDetection = true) - // end::snippet_3[] - class TestDetectionExample1 { - } + @ApplicationScoped + class MyService { - // tag::snippet_4[] - @HelidonTest(pinningDetection = true, pinningThreshold = 50)// <1> - // end::snippet_4[] - class TestDetectionExample2 { + String test() { + return "Not Mocked"; + } + } + // end::snippet_1[] } - } diff --git a/docs/src/main/java/io/helidon/docs/mp/testing/TestingSnippets.java b/docs/src/main/java/io/helidon/docs/mp/testing/TestingSnippets.java index dea7121d3b2..af08264151e 100644 --- a/docs/src/main/java/io/helidon/docs/mp/testing/TestingSnippets.java +++ b/docs/src/main/java/io/helidon/docs/mp/testing/TestingSnippets.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 Oracle and/or its affiliates. + * Copyright (c) 2024, 2025 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. @@ -16,21 +16,28 @@ package io.helidon.docs.mp.testing; import io.helidon.microprofile.config.ConfigCdiExtension; -import io.helidon.microprofile.testing.junit5.AddBean; -import io.helidon.microprofile.testing.junit5.AddConfig; -import io.helidon.microprofile.testing.junit5.AddExtension; -import io.helidon.microprofile.testing.junit5.AddJaxRs; -import io.helidon.microprofile.testing.junit5.DisableDiscovery; -import io.helidon.microprofile.testing.junit5.HelidonTest; +import io.helidon.microprofile.testing.AddConfigBlock; +import io.helidon.microprofile.testing.Configuration; +import io.helidon.microprofile.testing.Socket; +import io.helidon.microprofile.testing.AddBean; +import io.helidon.microprofile.testing.AddConfig; +import io.helidon.microprofile.testing.AddExtension; +import io.helidon.microprofile.testing.AddJaxRs; +import io.helidon.microprofile.testing.DisableDiscovery; +import io.helidon.microprofile.testing.mocking.MockBean; +import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.RequestScoped; import jakarta.inject.Inject; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.client.WebTarget; import jakarta.ws.rs.core.Response; -import org.junit.jupiter.api.Test; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.mockito.Answers; +import org.mockito.Mockito; +import static io.helidon.common.testing.virtualthreads.PinningRecorder.DEFAULT_THRESHOLD; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; @@ -39,68 +46,328 @@ class TestingSnippets { // stub - static class MyBean { + class MyBean { String greeting() { return ""; } } - // tag::snippet_1[] - @HelidonTest // <1> - @DisableDiscovery // <2> - @AddBean(MyBean.class) // <3> - @AddExtension(ConfigCdiExtension.class) // <4> - @AddConfig(key = "app.greeting", value = "TestHello") // <5> - class TestExample { - @Inject - private MyBean myBean; // <6> + // stub + class MyResource { + } + + // stub + @interface HelidonTest { + boolean resetPerTest() default false; + long pinningThreshold() default DEFAULT_THRESHOLD; + boolean pinningDetection() default false; + } + + // stub + @interface Test { + } + + class Snippet1 { + + // tag::snippet_1[] + @HelidonTest // <1> + class MyTest { + } + // end::snippet_1[] + } + + class Snippet2 { + + // tag::snippet_2[] + @DisableDiscovery // <1> + @AddBean(MyBean.class) // <2> + @HelidonTest + class MyTest { + } + // end::snippet_2[] + } + + class Snippet3 { + + // tag::snippet_3[] + @HelidonTest(resetPerTest = true) + class MyTest { + + @Test + void testOne() { // <1> + } + + @Test + void testTwo() { // <2> + } + } + // end::snippet_3[] + } + + class Snippet4 { + + // tag::snippet_4[] + @HelidonTest + class MyTest { + + @Test + void testOne() { // <1> + } + + @Test + @DisableDiscovery + @AddBean(MyBean.class) + void testTwo() { // <2> + } + } + // end::snippet_4[] + } + + class Snippet5 { + + // tag::snippet_5[] + @DisableDiscovery + @AddJaxRs // <1> + @AddBean(MyResource.class) // <2> + @HelidonTest + class MyTest { + } + // end::snippet_5[] + } - @Test - void testGreeting() { // <7> - assertThat(myBean, notNullValue()); - assertThat(myBean.greeting(), is("TestHello")); + class Snippet6 { + + // tag::snippet_6[] + @Configuration(useExisting = true) + @HelidonTest + class MyTest { } + // end::snippet_6[] } - // end::snippet_1[] - // tag::snippet_2[] - @HelidonTest // <1> - @DisableDiscovery // <2> - @AddJaxRs // <3> - @AddBean(TestReqScopeDisabledDiscovery.MyController.class) - public class TestReqScopeDisabledDiscovery { + class Snippet7 { - @Inject - private WebTarget target; + // tag::snippet_7[] + @AddConfig(key = "foo", value = "bar") + @HelidonTest + class MyTest { + } + // end::snippet_7[] + } + + class Snippet8 { - @Test - void testGet() { - String greeting = target.path("/greeting") - .request().get(String.class); - assertThat(greeting, is("Hallo!")); + // tag::snippet_8[] + @AddConfigBlock(""" + foo=bar + bob=alice + """) + @HelidonTest + class MyTest { + } + // end::snippet_8[] + } + + class Snippet9 { + + // tag::snippet_9[] + @AddConfigBlock(type = "yaml", value = """ + my-test: + foo: bar + bob: alice + """) + @HelidonTest + class MyTest { + } + // end::snippet_9[] + } + + class Snippet10 { + + // tag::snippet_10[] + @Configuration(configSources = { + "my-test1.yaml", + "my-test2.yaml" + }) + @HelidonTest + class MyTest { + } + // end::snippet_10[] + } + + class Snippet11 { + + // tag::snippet_11[] + @AddConfigBlock(value = """ + config_ordinal=120 + foo=bar + """) + @HelidonTest + class MyTest { + } + // end::snippet_11[] + } + + class Snippet12 { + + // tag::snippet_12[] + @HelidonTest + class MyTest { + + @Inject + @Socket("admin") + WebTarget target; + } + // end::snippet_12[] + } + + class Snippet13 { + + // tag::snippet_13[] + @HelidonTest + class MyTest { + + @Inject + WebTarget target; + } + // end::snippet_13[] + } + + class Snippet14 { + + // tag::snippet_14[] + @HelidonTest + class MyTest { + + @Test + void testOne(WebTarget target) { + } + } + // end::snippet_14[] + } + + class Snippet15 { + + // tag::snippet_15[] + @HelidonTest + @DisableDiscovery // <1> + @AddBean(MyBean.class) // <2> + @AddExtension(ConfigCdiExtension.class) // <3> + @AddConfig(key = "app.greeting", value = "TestHello") // <4> + class MyTest { + @Inject + MyBean myBean; + + @Test + void testGreeting() { + assertThat(myBean, notNullValue()); + assertThat(myBean.greeting(), is("TestHello")); + } + } + + @ApplicationScoped + class MyBean { + + @ConfigProperty(name = "app.greeting") // <5> + String greeting; + + String greeting() { + return greeting; + } + } + // end::snippet_15[] + } + + class Snippet16 { + + // tag::snippet_16[] + @HelidonTest + @DisableDiscovery // <1> + @AddJaxRs // <2> + @AddBean(MyResource.class) // <3> + class MyTest { + + @Inject + WebTarget target; + + @Test + void testGet() { + String greeting = target.path("/greeting") + .request().get(String.class); + assertThat(greeting, is("Hallo!")); + } } @Path("/greeting") - @RequestScoped // <4> - public static class MyController { + @RequestScoped + class MyResource { @GET - public Response get() { + Response get() { return Response.ok("Hallo!").build(); } } + // end::snippet_16[] } - // end::snippet_2[] - // tag::snippet_3[] - @HelidonTest(pinningDetection = true) - // end::snippet_3[] - class TestDetectionExample1 { + class Snippet17 { + + // tag::snippet_17[] + @HelidonTest + @AddBean(MyResource.class) + @AddBean(MyService.class) + class MyTest { + + @MockBean(answer = Answers.CALLS_REAL_METHODS) // <1> + MyService myService; + + @Inject + WebTarget target; + + @Test + void testService() { + Mockito.when(myService.test()).thenReturn("Mocked"); // <2> + String response = target.path("/test").request().get(String.class); + assertThat(response, is("Mocked")); + } + } + + @Path("/test") + class MyResource { + + @Inject + MyService myService; + + @GET + String test() { + return myService.test(); + } + } + + @ApplicationScoped + class MyService { + + String test() { + return "Not Mocked"; + } + } + // end::snippet_17[] } - // tag::snippet_4[] - @HelidonTest(pinningDetection = true, pinningThreshold = 50)// <1> - // end::snippet_4[] - class TestDetectionExample2 { + class Snippet18 { + + // tag::snippet_18[] + @HelidonTest(pinningDetection = true) + class MyTest { + } + // end::snippet_18[] } + class Snippet19 { + + // tag::snippet_19[] + @HelidonTest(pinningDetection = true, pinningThreshold = 50) // <1> + class MyTest { + } + // end::snippet_19[] + } } diff --git a/microprofile/fault-tolerance/src/test/java/io/helidon/microprofile/faulttolerance/RetryTest.java b/microprofile/fault-tolerance/src/test/java/io/helidon/microprofile/faulttolerance/RetryTest.java index 65d6908cdd0..49113222a96 100644 --- a/microprofile/fault-tolerance/src/test/java/io/helidon/microprofile/faulttolerance/RetryTest.java +++ b/microprofile/fault-tolerance/src/test/java/io/helidon/microprofile/faulttolerance/RetryTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2024 Oracle and/or its affiliates. + * Copyright (c) 2018, 2025 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. @@ -19,6 +19,7 @@ import java.io.IOException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; +import java.util.function.Supplier; import java.util.stream.Stream; import io.helidon.microprofile.testing.junit5.AddBean; @@ -36,17 +37,18 @@ */ @AddBean(RetryBean.class) @AddBean(SyntheticRetryBean.class) -public class RetryTest extends FaultToleranceTest { +class RetryTest extends FaultToleranceTest { static Stream createBeans() { return Stream.of( - Arguments.of(newBean(RetryBean.class), "ManagedRetryBean"), - Arguments.of(newNamedBean(SyntheticRetryBean.class), "SyntheticRetryBean")); + Arguments.of((Supplier) () -> newBean(RetryBean.class), "ManagedRetryBean"), + Arguments.of((Supplier) () -> newNamedBean(SyntheticRetryBean.class), "SyntheticRetryBean")); } @ParameterizedTest(name = "{1}") @MethodSource("createBeans") - public void testRetryBean(RetryBean bean, String unused) { + void testRetryBean(Supplier supplier, String unused) { + RetryBean bean = supplier.get(); bean.reset(); assertThat(bean.getInvocations(), is(0)); bean.retry(); @@ -55,7 +57,8 @@ public void testRetryBean(RetryBean bean, String unused) { @ParameterizedTest(name = "{1}") @MethodSource("createBeans") - public void testRetryBeanFallback(RetryBean bean, String unused) { + void testRetryBeanFallback(Supplier supplier, String unused) { + RetryBean bean = supplier.get(); bean.reset(); assertThat(bean.getInvocations(), is(0)); String value = bean.retryWithFallback(); @@ -65,7 +68,8 @@ public void testRetryBeanFallback(RetryBean bean, String unused) { @ParameterizedTest(name = "{1}") @MethodSource("createBeans") - public void testRetryAsync(RetryBean bean, String unused) throws Exception { + void testRetryAsync(Supplier supplier, String unused) throws Exception { + RetryBean bean = supplier.get(); bean.reset(); CompletableFuture future = bean.retryAsync(); future.get(); @@ -74,7 +78,8 @@ public void testRetryAsync(RetryBean bean, String unused) throws Exception { @ParameterizedTest(name = "{1}") @MethodSource("createBeans") - public void testRetryWithDelayAndJitter(RetryBean bean, String unused) throws Exception { + void testRetryWithDelayAndJitter(Supplier supplier, String unused) { + RetryBean bean = supplier.get(); bean.reset(); long millis = System.currentTimeMillis(); bean.retryWithDelayAndJitter(); @@ -84,13 +89,13 @@ public void testRetryWithDelayAndJitter(RetryBean bean, String unused) throws Ex /** * Inspired by a TCK test which makes sure failed executions propagate correctly. * - * @param bean the bean to invoke + * @param supplier supplier of the bean to invoke * @param unused bean name to use for the specific test invocation - * @throws Exception */ @ParameterizedTest(name = "{1}") @MethodSource("createBeans") - public void testRetryWithException(RetryBean bean, String unused) throws Exception { + void testRetryWithException(Supplier supplier, String unused) { + RetryBean bean = supplier.get(); bean.reset(); CompletionStage future = bean.retryWithException(); assertCompleteExceptionally(future.toCompletableFuture(), IOException.class, "Simulated error"); @@ -99,7 +104,8 @@ public void testRetryWithException(RetryBean bean, String unused) throws Excepti @ParameterizedTest(name = "{1}") @MethodSource("createBeans") - public void testRetryCompletionStageWithEventualSuccess(RetryBean bean, String unused) { + void testRetryCompletionStageWithEventualSuccess(Supplier supplier, String unused) { + RetryBean bean = supplier.get(); bean.reset(); assertCompleteOk(bean.retryWithUltimateSuccess(), "success"); assertThat(bean.getInvocations(), is(3)); @@ -107,7 +113,8 @@ public void testRetryCompletionStageWithEventualSuccess(RetryBean bean, String u @ParameterizedTest(name = "{1}") @MethodSource("createBeans") - public void testRetryWithCustomRuntimeException(RetryBean bean, String unused) { + void testRetryWithCustomRuntimeException(Supplier supplier, String unused) { + RetryBean bean = supplier.get(); bean.reset(); assertThat(bean.getInvocations(), is(0)); assertCompleteOk(bean.retryOnCustomRuntimeException(), "success"); diff --git a/microprofile/lra/jax-rs/src/test/java/io/helidon/microprofile/lra/LoadBalancedCoordinatorTest.java b/microprofile/lra/jax-rs/src/test/java/io/helidon/microprofile/lra/LoadBalancedCoordinatorTest.java index fe31cc26999..77320334bea 100644 --- a/microprofile/lra/jax-rs/src/test/java/io/helidon/microprofile/lra/LoadBalancedCoordinatorTest.java +++ b/microprofile/lra/jax-rs/src/test/java/io/helidon/microprofile/lra/LoadBalancedCoordinatorTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2023 Oracle and/or its affiliates. + * Copyright (c) 2021, 2025 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. @@ -52,6 +52,7 @@ import io.helidon.microprofile.testing.junit5.AddBean; import io.helidon.microprofile.testing.junit5.AddConfig; import io.helidon.microprofile.testing.junit5.AddExtension; +import io.helidon.microprofile.testing.junit5.AddJaxRs; import io.helidon.microprofile.testing.junit5.DisableDiscovery; import io.helidon.microprofile.testing.junit5.HelidonTest; import io.helidon.webclient.http1.Http1Client; @@ -66,11 +67,11 @@ import jakarta.ws.rs.core.UriBuilder; import org.eclipse.microprofile.lra.annotation.LRAStatus; import org.eclipse.microprofile.lra.annotation.ParticipantStatus; -import org.glassfish.jersey.ext.cdi1x.internal.CdiComponentProvider; import org.hamcrest.core.AnyOf; import org.hamcrest.core.IsNull; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; import static org.eclipse.microprofile.lra.annotation.ws.rs.LRA.LRA_HTTP_CONTEXT_HEADER; import static org.hamcrest.MatcherAssert.assertThat; @@ -88,11 +89,9 @@ */ @HelidonTest @DisableDiscovery +@AddJaxRs // Helidon MP @AddExtension(ConfigCdiExtension.class) -@AddExtension(ServerCdiExtension.class) -@AddExtension(JaxRsCdiExtension.class) -@AddExtension(CdiComponentProvider.class) // LRA client @AddExtension(LraCdiExtension.class) // resources diff --git a/microprofile/scheduling/src/test/java/io/helidon/microprofile/scheduling/SchedulingTest.java b/microprofile/scheduling/src/test/java/io/helidon/microprofile/scheduling/SchedulingTest.java index 16673f81c1f..81f66f93815 100644 --- a/microprofile/scheduling/src/test/java/io/helidon/microprofile/scheduling/SchedulingTest.java +++ b/microprofile/scheduling/src/test/java/io/helidon/microprofile/scheduling/SchedulingTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2023 Oracle and/or its affiliates. + * Copyright (c) 2021, 2025 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. @@ -24,9 +24,7 @@ import java.util.concurrent.atomic.AtomicInteger; import io.helidon.microprofile.testing.junit5.AddBean; -import io.helidon.microprofile.testing.junit5.AddBeans; import io.helidon.microprofile.testing.junit5.AddExtension; -import io.helidon.microprofile.testing.junit5.AddExtensions; import io.helidon.microprofile.testing.junit5.Configuration; import io.helidon.microprofile.testing.junit5.DisableDiscovery; import io.helidon.microprofile.testing.junit5.HelidonTest; @@ -43,12 +41,8 @@ @HelidonTest @DisableDiscovery -@AddBeans({ - @AddBean(ScheduledBean.class) -}) -@AddExtensions({ - @AddExtension(SchedulingCdiExtension.class), -}) +@AddBean(ScheduledBean.class) +@AddExtension(SchedulingCdiExtension.class) @Configuration(configSources = "test.properties") public class SchedulingTest { diff --git a/microprofile/testing/junit5/pom.xml b/microprofile/testing/junit5/pom.xml index 096574c1eed..c1a068e2238 100644 --- a/microprofile/testing/junit5/pom.xml +++ b/microprofile/testing/junit5/pom.xml @@ -33,10 +33,18 @@ + + io.helidon.microprofile.testing + helidon-microprofile-testing + + + io.helidon.testing + helidon-testing-junit5 + io.helidon.microprofile.server helidon-microprofile-server - true + provided io.helidon.common.testing @@ -47,14 +55,10 @@ helidon-microprofile-cdi provided - - io.helidon.testing - helidon-testing-junit5 - org.glassfish.jersey.ext.cdi jersey-weld2-se - true + provided org.junit.jupiter @@ -71,9 +75,8 @@ org.hamcrest - hamcrest-core + hamcrest-all test - diff --git a/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/AddBean.java b/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/AddBean.java index fa6ebbee729..9e4342a4169 100644 --- a/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/AddBean.java +++ b/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/AddBean.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2023 Oracle and/or its affiliates. + * Copyright (c) 2020, 2025 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. @@ -17,37 +17,45 @@ import java.lang.annotation.Annotation; import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import jakarta.enterprise.context.ApplicationScoped; - /** - * Add a bean. - * This is intended for test sources where we do not want to add {@code beans.xml} as this would add - * all test classes as beans. - * The bean will be added by default with {@link jakarta.enterprise.context.ApplicationScoped}. - * The class will be instantiated using CDI and will be available for injection into test classes and other beans. + * Add a CDI bean to the container. + *

* This annotation can be repeated. + *

+ * If used on a method, the container will be reset regardless of the test lifecycle. + *

+ * The bean scope is defined as follows: + *

    + *
  • If a scope is set with {@link #value()}, it overrides any scope defined on the bean
  • + *
  • Otherwise, the scope defined on the bean is used
  • + *
  • If the bean does not define a scope, {@link jakarta.enterprise.context.ApplicationScoped ApplicationScoped} + * is used
  • + *
+ * @deprecated Use {@link io.helidon.microprofile.testing.AddBean} instead */ +@Inherited @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.METHOD}) @Repeatable(AddBeans.class) +@Deprecated(since = "4.2.0") public @interface AddBean { /** - * Class of the bean. - * @return the class of a bean + * The bean class. + * + * @return bean class */ Class value(); /** - * Scope of the bean. - * Only {@link jakarta.inject.Singleton}, {@link jakarta.enterprise.context.ApplicationScoped} - * and {@link jakarta.enterprise.context.RequestScoped} scopes are supported. + * Override the bean scope. * - * @return scope of the bean + * @return scope class */ - Class scope() default ApplicationScoped.class; + Class scope() default Annotation.class; } diff --git a/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/AddBeans.java b/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/AddBeans.java index 77b9f845dbd..f8b9e1cf7dc 100644 --- a/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/AddBeans.java +++ b/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/AddBeans.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2022, 2025 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. @@ -16,21 +16,34 @@ package io.helidon.microprofile.testing.junit5; import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** - * A repeatable container for {@link io.helidon.microprofile.testing.junit5.AddBean}. - * No need to use this annotation, just repeat {@link io.helidon.microprofile.testing.junit5.AddBean} annotation - * on test class. + * A repeatable container for {@link AddBean}. + *

+ * This annotation is optional, you can instead repeat {@link AddBean}. + *

+ * E.g. + *

+ * @AddBean(FooBean.class)
+ * @AddBean(BarBean.class)
+ * class MyTest {
+ * }
+ * 
+ * @deprecated Use {@link io.helidon.microprofile.testing.AddBeans} instead */ +@Inherited @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.METHOD}) +@Deprecated(since = "4.2.0") public @interface AddBeans { /** - * Beans to be added. - * @return add bean annotations + * Get the contained annotations. + * + * @return annotations */ AddBean[] value(); } diff --git a/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/AddConfig.java b/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/AddConfig.java index a1c5c4da63a..3cb9aa8c6cb 100644 --- a/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/AddConfig.java +++ b/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/AddConfig.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2022, 2025 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. @@ -16,27 +16,40 @@ package io.helidon.microprofile.testing.junit5; import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** - * Add a configuration key/value pair to MicroProfile configuration. + * Add a configuration key/value pair to the {@link Configuration#useExisting() synthetic test configuration}. + *

* This annotation can be repeated. + *

+ * If used on a method, the container will be reset regardless of the test lifecycle. + * + * @see AddConfigs + * @see AddConfigBlock + * @see Configuration + * @deprecated Use {@link io.helidon.microprofile.testing.AddConfig} instead */ +@Inherited @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.METHOD}) @Repeatable(AddConfigs.class) +@Deprecated(since = "4.2.0") public @interface AddConfig { /** * Configuration property key. + * * @return key */ String key(); /** * Configuration property value. + * * @return value */ String value(); diff --git a/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/AddConfigBlock.java b/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/AddConfigBlock.java index 595cb8bed1d..8a952079719 100644 --- a/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/AddConfigBlock.java +++ b/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/AddConfigBlock.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 Oracle and/or its affiliates. + * Copyright (c) 2024, 2025 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. @@ -23,28 +23,35 @@ import java.lang.annotation.Target; /** - * Defines the configuration as a String in {@link #value()} for the - * given type. + * Add a configuration fragment to the {@link Configuration#useExisting() synthetic test configuration}. + *

+ * This annotation can be repeated. + *

+ * If used on a method, the container will be reset regardless of the test lifecycle. + * + * @see AddConfig + * @see AddConfigs + * @see Configuration + * @deprecated Use {@link io.helidon.microprofile.testing.AddConfigBlock} instead */ +@Inherited @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.METHOD}) -@Inherited +@Deprecated(since = "4.2.0") public @interface AddConfigBlock { - /** - * Specifies the format type of the {@link #value()}. - * - * It defaults to 'properties'. + * Specifies the configuration format. + *

+ * The default format is 'properties' * * @return the supported type */ String type() default "properties"; /** - * Configuration value. + * Configuration fragment. * - * @return String with value. + * @return fragment */ String value(); - } diff --git a/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/AddConfigs.java b/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/AddConfigs.java index 82553a8dd0f..4fbdb280e7e 100644 --- a/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/AddConfigs.java +++ b/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/AddConfigs.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2022, 2025 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. @@ -16,22 +16,37 @@ package io.helidon.microprofile.testing.junit5; import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** - * A repeatable container for {@link io.helidon.microprofile.testing.junit5.AddConfig}. - * No need to use this annotation, just repeat {@link io.helidon.microprofile.testing.junit5.AddConfig} annotation - * on test class. + * A repeatable container for {@link AddConfig}. + *

+ * This annotation is optional, you can instead repeat {@link AddConfig}. + *

+ * E.g. + *

+ * @AddConfig(key="foo", value="1")
+ * @AddConfig(key="bar", value="2")
+ * class MyTest {
+ * }
+ * 
+ * + * @see AddConfig + * @see Configuration + * @deprecated Use {@link io.helidon.microprofile.testing.AddConfigs} instead */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.METHOD}) +@Inherited +@Deprecated(since = "4.2.0") public @interface AddConfigs { /** - * Configuration properties to be added. + * Get the contained annotations. * - * @return properties + * @return annotations */ AddConfig[] value(); } diff --git a/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/AddExtension.java b/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/AddExtension.java index c81db4b784b..850440b812f 100644 --- a/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/AddExtension.java +++ b/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/AddExtension.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2023 Oracle and/or its affiliates. + * Copyright (c) 2020, 2025 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. @@ -16,6 +16,7 @@ package io.helidon.microprofile.testing.junit5; import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -24,15 +25,22 @@ import jakarta.enterprise.inject.spi.Extension; /** - * Add a CDI extension to the test container. + * Add a CDI extension to the container. + *

* This annotation can be repeated. + *

+ * If used on a method, the container will be reset regardless of the test lifecycle. + * @deprecated Use {@link io.helidon.microprofile.testing.AddExtension} instead */ +@Inherited @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.METHOD}) @Repeatable(AddExtensions.class) +@Deprecated(since = "4.2.0") public @interface AddExtension { /** * Class of the extension to add. The class must be public. + * * @return extension class. */ Class value(); diff --git a/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/AddExtensions.java b/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/AddExtensions.java index 565f7f0d2f6..14b5fa99687 100644 --- a/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/AddExtensions.java +++ b/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/AddExtensions.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2022, 2025 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. @@ -16,21 +16,34 @@ package io.helidon.microprofile.testing.junit5; import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** - * A repeatable container for {@link io.helidon.microprofile.testing.junit5.AddExtension}. - * No need to use this annotation, just repeat {@link io.helidon.microprofile.testing.junit5.AddExtension} annotation - * on test class. + * A repeatable container for {@link AddExtension}. + *

+ * This annotation is optional, you can instead repeat {@link AddExtension}. + *

+ * E.g. + *

+ * @AddExtension(FooExtension.class)
+ * @AddExtension(BarExtension.class)
+ * class MyTest {
+ * }
+ * 
+ * @deprecated Use {@link io.helidon.microprofile.testing.AddExtensions} instead */ +@Inherited @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.METHOD}) +@Deprecated(since = "4.2.0") public @interface AddExtensions { /** - * Extensions to be added. - * @return extensions + * Get the contained annotations. + * + * @return annotations */ AddExtension[] value(); } diff --git a/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/AddJaxRs.java b/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/AddJaxRs.java index 1f776358f0d..6311d4e82ee 100644 --- a/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/AddJaxRs.java +++ b/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/AddJaxRs.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2025 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. @@ -16,14 +16,32 @@ package io.helidon.microprofile.testing.junit5; import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import io.helidon.microprofile.server.JaxRsCdiExtension; +import io.helidon.microprofile.server.ServerCdiExtension; + +import org.glassfish.jersey.ext.cdi1x.internal.CdiComponentProvider; +import org.glassfish.jersey.ext.cdi1x.internal.ProcessAllAnnotatedTypes; +import org.glassfish.jersey.weld.se.WeldRequestScope; + /** - * Add JaxRS support for Request-scoped beans. + * Add JAX-RS (Jersey) support. + *

+ * If used on a method, the container will be reset regardless of the test lifecycle. + * @deprecated Use {@link io.helidon.microprofile.testing.AddJaxRs} instead */ +@Inherited @Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.TYPE}) +@Target({ElementType.TYPE, ElementType.METHOD}) +@AddExtension(ProcessAllAnnotatedTypes.class) +@AddExtension(ServerCdiExtension.class) +@AddExtension(JaxRsCdiExtension.class) +@AddExtension(CdiComponentProvider.class) +@AddBean(WeldRequestScope.class) +@Deprecated(since = "4.2.0") public @interface AddJaxRs { } diff --git a/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/AfterStop.java b/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/AfterStop.java index ae3225d462a..80fdc972207 100644 --- a/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/AfterStop.java +++ b/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/AfterStop.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2025 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. @@ -17,14 +17,25 @@ package io.helidon.microprofile.testing.junit5; import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * Mark a static method to be executed after the container is stopped. + *

+ * E.g. + *

+ * @AfterStop
+ * static void afterStop() {
+ *     // ...
+ * }
+ * @deprecated Use {@link io.helidon.microprofile.testing.AfterStop} instead */ +@Inherited @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) +@Deprecated(since = "4.2.0") public @interface AfterStop { } diff --git a/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/Configuration.java b/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/Configuration.java index f75de831b10..c0badc2fab7 100644 --- a/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/Configuration.java +++ b/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/Configuration.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2023 Oracle and/or its affiliates. + * Copyright (c) 2020, 2025 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. @@ -23,38 +23,55 @@ import java.lang.annotation.Target; /** - * Additional configuration of config itself. + * General setting for the test configuration. + *

+ * If used on a method, the container will be reset regardless of the test lifecycle. + * + * @see AddConfig + * @see AddConfigs + * @see AddConfigBlock + * @deprecated Use {@link io.helidon.microprofile.testing.Configuration} instead */ +@Inherited @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.METHOD}) -@Inherited +@Deprecated(since = "4.2.0") public @interface Configuration { /** - * If set to {@code true}, the existing (or default) MicroProfile configuration would be used. In this case it is - * important to set property {@code mp.initializer.allow=true} in order CDI container to start, when used with - * {@link HelidonTest}. - * By default uses a configuration constructed using all {@link io.helidon.microprofile.testing.junit5.AddConfig} - * annotations and {@link #configSources()}. - * When set to false and a {@link org.junit.jupiter.api.BeforeAll} method registers a custom configuration - * with {@link org.eclipse.microprofile.config.spi.ConfigProviderResolver}, the result is undefined, though - * tests have shown that the registered config may be used (as BeforeAll ordering is undefined by - * JUnit, it may be called after our extension) + * If set to {@code false}, the synthetic test configuration is used. + *

+ * The synthetic test configuration is expressed with the following: + *

    + *
  • {@link #configSources()}
  • + *
  • {@link #profile()}
  • + *
  • {@link AddConfig}
  • + *
  • {@link AddConfigs}
  • + *
  • {@link AddConfigBlock}
  • + *
+ *

+ * If set to {@code true}, only the existing (or default) MicroProfile configuration is used as-is + * and the annotations listed previously are ignored. + *

+ * You can use {@link org.eclipse.microprofile.config.spi.ConfigProviderResolver ConfigProviderResolver} to define + * the configuration programmatically before the CDI container starts. * - * @return whether to use existing (or default) configuration, or customized one + * @return whether to use existing (or default) configuration */ boolean useExisting() default false; /** - * Class path properties config sources to add to configuration of this test class or method. + * Class-path resources to add as config sources to the synthetic test configuration. * - * @return config sources to add + * @return config sources */ String[] configSources() default {}; /** * Configuration profile. + *

+ * The default profile is 'test' * - * @return String with default value "test". + * @return profile */ String profile() default "test"; } diff --git a/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/DisableDiscovery.java b/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/DisableDiscovery.java index 08c6c2b7b94..6eb786278e6 100644 --- a/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/DisableDiscovery.java +++ b/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/DisableDiscovery.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2022, 2025 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. @@ -23,30 +23,39 @@ import java.lang.annotation.Target; /** - * Whether discovery is automated or disabled. If discovery is desired, do not annotate test - * class with this annotation. + * Disables CDI discovery. *

- * When discovery is enabled, the whole classpath is scanned for bean archives (jar files containing - * {@code META-INF/beans.xml}) and all beans and extensions are added automatically. + * If discovery is desired, do not annotate test class with this annotation. *

- * When discovery is disabled, CDI would only contain the CDI implementation itself and beans and extensions added - * through annotations {@link io.helidon.microprofile.testing.junit5.AddBean} and - * {@link io.helidon.microprofile.testing.junit5.AddExtension} - * - * If discovery is disabled on class level and desired on method level, - * the value can be set to {@code false}. + * If used on a method, the container will be reset regardless of the test lifecycle. + *

+ * When disabling discovery, you are responsible for adding the beans and extensions needed to activate the features you need. + * You can use the following annotations to do that: + *

    + *
  • {@link AddBean} to add CDI beans
  • + *
  • {@link AddExtension} to add CDI extensions
  • + *
  • {@link AddJaxRs} a shorthand to add JAX-RS (Jersey)
  • + *
+ *

+ * See also the following "core" CDI extensions: + *

    + *
  • {@link io.helidon.microprofile.server.ServerCdiExtension ServerCdiExtension} optional if using {@link AddJaxRs}
  • + *
  • {@link io.helidon.microprofile.server.JaxRsCdiExtension JaxRsCdiExtension} optional if using {@link AddJaxRs}
  • + *
  • {@link io.helidon.microprofile.config.ConfigCdiExtension ConfigCdiExtension}
  • + *
*/ +@Inherited @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.METHOD}) -@Inherited +@Deprecated(since = "4.2.0") public @interface DisableDiscovery { /** - * By default if you annotate a class or a method, discovery gets disabled. + * By default, if you annotate a class or a method, discovery gets disabled. * If you want to override configuration on method to differ from class, you * can configure the value to {@code false}, effectively enabling discovery. * * @return whether to disable discovery ({@code true}), or enable it ({@code false}). If this - * annotation is not present, discovery is enabled + * annotation is not present, discovery is enabled */ boolean value() default true; } diff --git a/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/HelidonImplicitResetOrderer.java b/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/HelidonImplicitResetOrderer.java new file mode 100644 index 00000000000..bf0c838effa --- /dev/null +++ b/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/HelidonImplicitResetOrderer.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2025 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.testing.junit5; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.ListIterator; + +import io.helidon.microprofile.testing.HelidonTestInfo; +import io.helidon.microprofile.testing.HelidonTestInfo.ClassInfo; +import io.helidon.microprofile.testing.HelidonTestInfo.MethodInfo; + +import org.junit.jupiter.api.MethodDescriptor; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.MethodOrdererContext; + +/** + * A method orderer that orders the methods that requires their own container last. + * The resulting ordering groups the methods that share the same container in order to avoid a restart. + * + * @see MethodInfo#requiresReset() + */ +public class HelidonImplicitResetOrderer implements MethodOrderer { + + @Override + public void orderMethods(MethodOrdererContext context) { + sort(context.getMethodDescriptors()); + } + + private static void sort(List descriptors) { + List shared = new ArrayList<>(); + List exclusive = new ArrayList<>(); + for (T e : descriptors) { + Method method = e.getMethod(); + ClassInfo classInfo = HelidonTestInfo.classInfo(method.getDeclaringClass(), HelidonTestDescriptorImpl::new); + MethodInfo methodInfo = HelidonTestInfo.methodInfo(method, classInfo, HelidonTestDescriptorImpl::new); + // only update order for implicit reset + if (!classInfo.resetPerTest() && methodInfo.requiresReset()) { + exclusive.add(e); + } else { + shared.add(e); + } + } + if (!exclusive.isEmpty()) { + ListIterator i = descriptors.listIterator(); + for (T e : shared) { + i.next(); + i.set(e); + } + for (T e : exclusive) { + i.next(); + i.set(e); + } + } + } +} 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 e64ea028088..a8f00c7da33 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 @@ -16,819 +16,228 @@ package io.helidon.microprofile.testing.junit5; -import java.io.IOException; -import java.io.Serial; -import java.io.StringReader; -import java.lang.annotation.Annotation; -import java.lang.reflect.AnnotatedElement; -import java.lang.reflect.Array; -import java.lang.reflect.Constructor; -import java.lang.reflect.Executable; -import java.lang.reflect.Field; import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.net.URL; -import java.time.Duration; -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 java.util.concurrent.locks.ReentrantLock; -import io.helidon.common.context.Context; -import io.helidon.common.testing.virtualthreads.PinningRecorder; -import io.helidon.config.mp.MpConfigSources; -import io.helidon.microprofile.server.JaxRsCdiExtension; -import io.helidon.microprofile.server.ServerCdiExtension; +import io.helidon.microprofile.testing.HelidonTestInfo.ClassInfo; +import io.helidon.microprofile.testing.HelidonTestInfo.MethodInfo; +import io.helidon.microprofile.testing.HelidonTestScope; +import io.helidon.microprofile.testing.Instrumented; import io.helidon.testing.junit5.TestJunitExtension; -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.enterprise.context.Dependent; -import jakarta.enterprise.context.RequestScoped; -import jakarta.enterprise.event.Observes; -import jakarta.enterprise.inject.se.SeContainer; -import jakarta.enterprise.inject.se.SeContainerInitializer; -import jakarta.enterprise.inject.spi.AfterBeanDiscovery; -import jakarta.enterprise.inject.spi.BeforeBeanDiscovery; -import jakarta.enterprise.inject.spi.CDI; -import jakarta.enterprise.inject.spi.Extension; -import jakarta.enterprise.inject.spi.InjectionPoint; -import jakarta.enterprise.inject.spi.ProcessInjectionPoint; -import jakarta.enterprise.inject.spi.configurator.AnnotatedTypeConfigurator; -import jakarta.enterprise.util.AnnotationLiteral; -import jakarta.inject.Inject; -import jakarta.inject.Singleton; -import jakarta.ws.rs.client.Client; -import jakarta.ws.rs.client.ClientBuilder; -import jakarta.ws.rs.client.WebTarget; -import org.eclipse.microprofile.config.Config; -import org.eclipse.microprofile.config.spi.ConfigBuilder; -import org.eclipse.microprofile.config.spi.ConfigProviderResolver; -import org.eclipse.microprofile.config.spi.ConfigSource; -import org.glassfish.jersey.ext.cdi1x.internal.CdiComponentProvider; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInstance; -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.TestInstance.Lifecycle; import org.junit.jupiter.api.extension.BeforeEachCallback; import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ExtensionContext.Store; +import org.junit.jupiter.api.extension.ExtensionContext.Store.CloseableResource; import org.junit.jupiter.api.extension.InvocationInterceptor; import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.api.extension.ParameterResolutionException; import org.junit.jupiter.api.extension.ParameterResolver; import org.junit.jupiter.api.extension.ReflectiveInvocationContext; +import org.junit.jupiter.api.extension.TestInstanceFactory; +import org.junit.jupiter.api.extension.TestInstanceFactoryContext; +import org.junit.jupiter.api.parallel.ExecutionMode; +import static io.helidon.microprofile.testing.HelidonTestInfo.classInfo; +import static io.helidon.microprofile.testing.HelidonTestInfo.methodInfo; +import static io.helidon.microprofile.testing.Instrumented.instrument; +import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_METHOD; /** - * Junit5 extension to support Helidon CDI container in tests. + * A JUnit5 extension that integrates CDI with JUnit to support Helidon MP. + *

+ * This extension starts a CDI container and adds the test class as a bean with support for injection. The test class uses + * a CDI scope that follows the test lifecycle as defined by {@link org.junit.jupiter.api.TestInstance TestInstance}. + *

+ * The container is started lazily during test execution to ensure that it is started after all other extensions. + *

+ * The container can be customized with the following annotations: + *

    + *
  • {@link HelidonTest#resetPerTest()} force a new CDI container per test
  • + *
  • {@link io.helidon.microprofile.testing.DisableDiscovery} disables CDI discovery
  • + *
  • {@link io.helidon.microprofile.testing.AddBean} add CDI beans
  • + *
  • {@link io.helidon.microprofile.testing.AddExtension} add CDI extension
  • + *
  • {@link io.helidon.microprofile.testing.AddJaxRs} add JAX-RS (Jersey)
  • + *
+ *

+ * The configuration can be customized with the following annotations: + *

    + *
  • {@link io.helidon.microprofile.testing.Configuration} global setting for MicroProfile configuration
  • + *
  • {@link io.helidon.microprofile.testing.AddConfig} declarative key/value pair configuration
  • + *
  • {@link io.helidon.microprofile.testing.AddConfigBlock} declarative fragment configuration
  • + *
+ *

+ * See also {@link io.helidon.microprofile.testing.Socket}, a CDI qualifier to inject JAX-RS client or URI. + *

+ * The container is created per test class by default, unless + * {@link HelidonTest#resetPerTest()} is {@code true}, in + * which case the container is created per test method. + *

+ * The container and the configuration can be customized per method regardless of the value of + * {@link HelidonTest#resetPerTest()}. The container will be reset accordingly. + *

+ * It is not recommended to provide a {@code beans.xml} along the test classes, as it would combine beans from all tests. + * Instead, you should use {@link io.helidon.microprofile.testing.AddBean} to specify the beans per test or method. + * + * @see HelidonTest */ -class HelidonJunitExtension extends TestJunitExtension - implements BeforeAllCallback, - AfterAllCallback, - BeforeEachCallback, - AfterEachCallback, +public class HelidonJunitExtension extends TestJunitExtension + implements BeforeEachCallback, + TestInstanceFactory, InvocationInterceptor, ParameterResolver { - private static final Set> HELIDON_TEST_ANNOTATIONS = - Set.of(AddBean.class, AddConfig.class, AddExtension.class, Configuration.class, - AddJaxRs.class, AddConfigBlock.class); - private static final Map, Annotation> BEAN_DEFINING = new HashMap<>(); - - static { - BEAN_DEFINING.put(ApplicationScoped.class, ApplicationScoped.Literal.INSTANCE); - BEAN_DEFINING.put(Singleton.class, ApplicationScoped.Literal.INSTANCE); - BEAN_DEFINING.put(RequestScoped.class, RequestScoped.Literal.INSTANCE); - BEAN_DEFINING.put(Dependent.class, Dependent.Literal.INSTANCE); - } - - private final List classLevelExtensions = new ArrayList<>(); - private final List classLevelBeans = new ArrayList<>(); - private final ConfigMeta classLevelConfigMeta = new ConfigMeta(); - private boolean classLevelDisableDiscovery = false; - private boolean resetPerTest; - private Class testClass; - private ConfigProviderResolver configProviderResolver; - private Config config; - private SeContainer container; - private PinningRecorder pinningRecorder; + private final ReentrantLock lock = new ReentrantLock(); @Override - public void beforeAll(ExtensionContext context) { - super.beforeAll(context); - - run(context, () -> { - testClass = context.getRequiredTestClass(); - - List metaAnnotations = extractMetaAnnotations(testClass); - - AddConfig[] configs = getAnnotations(testClass, AddConfig.class, metaAnnotations); - classLevelConfigMeta.addConfig(configs); - classLevelConfigMeta.configuration(getAnnotation(testClass, Configuration.class, metaAnnotations)); - classLevelConfigMeta.addConfigBlock(getAnnotation(testClass, AddConfigBlock.class, metaAnnotations)); - configProviderResolver = ConfigProviderResolver.instance(); - - AddExtension[] extensions = getAnnotations(testClass, AddExtension.class, metaAnnotations); - classLevelExtensions.addAll(Arrays.asList(extensions)); - - AddBean[] beans = getAnnotations(testClass, AddBean.class, metaAnnotations); - classLevelBeans.addAll(Arrays.asList(beans)); - - HelidonTest testAnnot = testClass.getAnnotation(HelidonTest.class); - if (testAnnot != null) { - resetPerTest = testAnnot.resetPerTest(); - if (testAnnot.pinningDetection()) { - pinningRecorder = PinningRecorder.create(); - pinningRecorder.record(Duration.ofMillis(testAnnot.pinningThreshold())); - } - } - - DisableDiscovery discovery = getAnnotation(testClass, DisableDiscovery.class, metaAnnotations); - if (discovery != null) { - classLevelDisableDiscovery = discovery.value(); - } - - if (resetPerTest) { - validatePerTest(); - - return; - } - validatePerClass(); - - // add beans when using JaxRS - AddJaxRs addJaxRsAnnotation = getAnnotation(testClass, AddJaxRs.class, metaAnnotations); - if (addJaxRsAnnotation != null) { - classLevelExtensions.add(ProcessAllAnnotatedTypesLiteral.INSTANCE); - classLevelExtensions.add(ServerCdiExtensionLiteral.INSTANCE); - classLevelExtensions.add(JaxRsCdiExtensionLiteral.INSTANCE); - classLevelExtensions.add(CdiComponentProviderLiteral.INSTANCE); - classLevelBeans.add(WeldRequestScopeLiteral.INSTANCE); - } - - configure(classLevelConfigMeta); - - if (!classLevelConfigMeta.useExisting) { - // the container startup is delayed in case we `useExisting`, so the is first set up by the user - // when we do not need to `useExisting`, we want to start early, so parameterized test method sources that use CDI - // can work - startContainer(classLevelBeans, classLevelExtensions, classLevelDisableDiscovery); - } + public Object createTestInstance(TestInstanceFactoryContext fc, ExtensionContext ctx) { + initStaticContext(ctx); + return supplyChecked(ctx, () -> { + // Instrument the test class + // Use a proxy to start the container lazily + Class testClass = instrument(ctx.getRequiredTestClass(), List.of(), List.of(), + (type, method) -> { + // class context store specific to the intercepted method + Store store = store(ctx, method); + return requiredContainer(store).resolveInstance(type); + }); + return Instrumented.allocateInstance(testClass); }); } - 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(Annotation::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.getFirst(); - } - } - return annotation; - } - - @SuppressWarnings("unchecked") - 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) { - directAnnotations = superClass.getAnnotationsByType(annotClass); - allAnnotations.addAll(List.of(directAnnotations)); - superClass = superClass.getSuperclass(); - } - - Object result = Array.newInstance(annotClass, allAnnotations.size()); - for (int i = 0; i < allAnnotations.size(); i++) { - Array.set(result, i, allAnnotations.get(i)); - } + @Override + public void interceptBeforeEachMethod(Invocation invocation, + ReflectiveInvocationContext ic, + ExtensionContext ctx) throws Throwable { - return (T[]) result; - } - - @SuppressWarnings("unchecked") - 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; + invoke(invocation, ic, ctx); } @Override - public void beforeEach(ExtensionContext context) { - if (resetPerTest) { - Method method = context.getRequiredTestMethod(); + public void interceptAfterEachMethod(Invocation invocation, + ReflectiveInvocationContext ic, + ExtensionContext ctx) throws Throwable { - Context helidonContext = Context.builder() - .id("test-" + testClass.getName() + "-" + System.identityHashCode(testClass) - + "-" + System.identityHashCode(method)) - .build(); - super.context(context, helidonContext); - - super.run(context, () -> { - AddConfig[] configs = method.getAnnotationsByType(AddConfig.class); - ConfigMeta methodLevelConfigMeta = classLevelConfigMeta.nextMethod(); - methodLevelConfigMeta.addConfig(configs); - methodLevelConfigMeta.configuration(method.getAnnotation(Configuration.class)); - methodLevelConfigMeta.addConfigBlock(method.getAnnotation(AddConfigBlock.class)); - - configure(methodLevelConfigMeta); - - List methodLevelExtensions = new ArrayList<>(classLevelExtensions); - List methodLevelBeans = new ArrayList<>(classLevelBeans); - boolean methodLevelDisableDiscovery = classLevelDisableDiscovery; - - AddExtension[] extensions = method.getAnnotationsByType(AddExtension.class); - methodLevelExtensions.addAll(Arrays.asList(extensions)); - - AddBean[] beans = method.getAnnotationsByType(AddBean.class); - methodLevelBeans.addAll(Arrays.asList(beans)); - - DisableDiscovery discovery = method.getAnnotation(DisableDiscovery.class); - if (discovery != null) { - methodLevelDisableDiscovery = discovery.value(); - } - - startContainer(methodLevelBeans, methodLevelExtensions, methodLevelDisableDiscovery); - }); - } + invoke(invocation, ic, ctx); } @Override - public void afterEach(ExtensionContext context) { + public void beforeEach(ExtensionContext context) { run(context, () -> { - if (resetPerTest) { - releaseConfig(); - stopContainer(); - } - }); -// pinningRecorder.checkAndThrow(); - } + Method testMethod = context.getRequiredTestMethod(); + Class testClass = context.getRequiredTestClass(); - private void validatePerClass() { - Method[] methods = testClass.getMethods(); - for (Method method : methods) { - if (method.getAnnotation(Test.class) != null) { - // a test method - if (hasHelidonTestAnnotation(method)) { - throw new RuntimeException("When a class is annotated with @HelidonTest, " - + "there is a single CDI container used to invoke all " - + "test methods on the class. Method " + method - + " has an annotation that modifies container behavior."); - } - } - } - - methods = testClass.getDeclaredMethods(); - for (Method method : methods) { - if (method.getAnnotation(Test.class) != null) { - // a test method - if (hasHelidonTestAnnotation(method)) { - throw new RuntimeException("When a class is annotated with @HelidonTest, " - + "there is a single CDI container used to invoke all " - + "test methods on the class. Method " + method - + " has an annotation that modifies container behavior."); - } - } - } - - AddJaxRs addJaxRsAnnotation = testClass.getAnnotation(AddJaxRs.class); - if (addJaxRsAnnotation != null){ - if (testClass.getAnnotation(DisableDiscovery.class) == null){ - throw new RuntimeException("@AddJaxRs annotation should be used only with @DisableDiscovery annotation."); - } - } - } - - private boolean hasHelidonTestAnnotation(AnnotatedElement element) { - for (Class aClass : HELIDON_TEST_ANNOTATIONS) { - if (element.getAnnotation(aClass) != null) { - return true; - } - } - return false; - } + ClassInfo classInfo = classInfo(testClass, HelidonTestDescriptorImpl::new); + MethodInfo methodInfo = methodInfo(testMethod, classInfo, HelidonTestDescriptorImpl::new); - private void validatePerTest() { - Constructor[] constructors = testClass.getConstructors(); - if (constructors.length > 1) { - throw new RuntimeException("When a class is annotated with @HelidonTest(resetPerTest=true)," - + " the class must have only a single no-arg constructor"); - } - if (constructors.length == 1) { - Constructor c = constructors[0]; - if (c.getParameterCount() > 0) { - throw new RuntimeException("When a class is annotated with @HelidonTest(resetPerTest=true)," - + " the class must have a no-arg constructor"); - } - } + ExtensionContext classContext = classContext(context); + Store classStore = store(classContext); + HelidonTestContainerImpl container = container(classStore); - Field[] fields = testClass.getFields(); - for (Field field : fields) { - if (field.getAnnotation(Inject.class) != null) { - throw new RuntimeException("When a class is annotated with @HelidonTest(resetPerTest=true)," - + " injection into fields or constructor is not supported, as each" - + " test method uses a different CDI container. Field " + field - + " is annotated with @Inject"); - } - } + if (context.getExecutionMode() == ExecutionMode.SAME_THREAD + && container != null && !container.closed() + && methodInfo.requiresReset()) { - fields = testClass.getDeclaredFields(); - for (Field field : fields) { - if (field.getAnnotation(Inject.class) != null) { - throw new RuntimeException("When a class is annotated with @HelidonTest(resetPerTest=true)," - + " injection into fields or constructor is not supported, as each" - + " test method uses a different CDI container. Field " + field - + " is annotated with @Inject"); + // close the "class container" only for sequential executions + // parallel & requireReset use multiple containers + container.close(); } - } - } - - private void configure(ConfigMeta configMeta) { - if (config != null) { - configProviderResolver.releaseConfig(config); - } - if (!configMeta.useExisting) { - // only create a custom configuration if not provided by test method/class - // prepare configuration - ConfigBuilder builder = configProviderResolver.getBuilder(); - configMeta.additionalSources.forEach(it -> { - String fileName = it.trim(); - int idx = fileName.lastIndexOf('.'); - String type = idx > -1 ? fileName.substring(idx + 1) : "properties"; - try { - Enumeration urls = Thread.currentThread().getContextClassLoader().getResources(fileName); - urls.asIterator().forEachRemaining(url -> builder.withSources(MpConfigSources.create(type, url))); - } catch (IOException e) { - throw new IllegalStateException("Failed to read \"" + fileName + "\" from classpath", e); + if (container == null || container.closed()) { + Store methodStore = store(context); + Lifecycle lifecycle = context.getTestInstanceLifecycle().orElse(PER_METHOD); + HelidonTestScope scope; + if (lifecycle == Lifecycle.PER_CLASS) { + scope = HelidonTestScope.ofContainer(); + } else { + scope = HelidonTestScope.ofThread(); + // put the scope in the method context store to auto-close + methodStore.put("scope", (CloseableResource) scope::close); + } + if (methodInfo.requiresReset()) { + // put in the method store to auto-close + container = new HelidonTestContainerImpl(methodInfo, scope); + methodStore.put("container", container); + } else { + // put the "class container" in the class context store + // to re-use between methods + lock.lock(); + try { + container = container(classStore); + if (container == null || container.closed()) { + container = new HelidonTestContainerImpl(classInfo, scope); + classStore.put("container", container); + } + } finally { + lock.unlock(); + } } - }); - if (configMeta.type != null && configMeta.block != null) { - builder.withSources(MpConfigSources.create(configMeta.type, new StringReader(configMeta.block))); - } - config = builder - .withSources(MpConfigSources.create(configMeta.additionalKeys)) - .addDefaultSources() - .addDiscoveredSources() - .addDiscoveredConverters() - .build(); - configProviderResolver.registerConfig(config, Thread.currentThread().getContextClassLoader()); - } - } - - private void releaseConfig() { - if (configProviderResolver != null && config != null) { - configProviderResolver.releaseConfig(config); - config = null; - } - } - - @SuppressWarnings("unchecked") - private void startContainer(List beanAnnotations, - List extensionAnnotations, - boolean disableDiscovery) { - - // now let's prepare the CDI bootstrapping - SeContainerInitializer initializer = SeContainerInitializer.newInstance(); - - if (disableDiscovery) { - initializer.disableDiscovery(); - } - - initializer.addExtensions(new AddBeansExtension(testClass, beanAnnotations)); - - for (AddExtension addExtension : extensionAnnotations) { - Class extensionClass = addExtension.value(); - if (Modifier.isPublic(extensionClass.getModifiers())) { - initializer.addExtensions(addExtension.value()); - } else { - throw new IllegalArgumentException("Extension classes must be public, but " + extensionClass - .getName() + " is not"); - } - } - - container = initializer.initialize(); - } - - private void stopContainer() { - if (container != null) { - container.close(); - container = null; - } - } - - @Override - public void afterAll(ExtensionContext context) { - run(context, () -> { - stopContainer(); - releaseConfig(); - callAfterStop(); - if (pinningRecorder != null) { - pinningRecorder.close(); - pinningRecorder = null; } + // proxy handler uses class context + // hence we use a class context store specific to the test method + store(classContext, testMethod).put("container", container); }); - super.afterAll(context); } @Override - public T interceptTestClassConstructor(Invocation invocation, - ReflectiveInvocationContext> invocationContext, - ExtensionContext extensionContext) throws Throwable { - - return super.supplyChecked(extensionContext, () -> { - if (resetPerTest) { - // Junit creates test instance - return invocation.proceed(); - } - - // we need to start container before the test class is instantiated, to honor @BeforeAll that - // creates a custom MP config - if (container == null) { - // at this early stage the class should be checked whether it is annotated with - // @TestInstance(TestInstance.Lifecycle.PER_CLASS) to start correctly the container - TestInstance testClassAnnotation = testClass.getAnnotation(TestInstance.class); - if (testClassAnnotation != null && testClassAnnotation.value().equals(TestInstance.Lifecycle.PER_CLASS)) { - throw new RuntimeException("When a class is annotated with @HelidonTest, " - + "it is not compatible with @" - + "TestInstance(TestInstance.Lifecycle.PER_CLASS)" - + "annotation, as it is a Singleton CDI Bean."); - } - startContainer(classLevelBeans, classLevelExtensions, classLevelDisableDiscovery); - } - - // we need to replace instantiation with CDI lookup, to properly injection into fields (and constructors) - invocation.skip(); + public boolean supportsParameter(ParameterContext pc, ExtensionContext ctx) + throws ParameterResolutionException { - return container.select(invocationContext.getExecutable().getDeclaringClass()) - .get(); + return supplyChecked(ctx, () -> { + Store store = store(ctx, ctx.getRequiredTestMethod()); + HelidonTestContainerImpl container = requiredContainer(store); + return !container.initFailed() && container.isSupported(pc.getParameter().getType()); }); } @Override - public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) + public Object resolveParameter(ParameterContext pc, ExtensionContext ctx) throws ParameterResolutionException { - return super.supplyChecked(extensionContext, () -> { - Executable executable = parameterContext.getParameter().getDeclaringExecutable(); - - if (resetPerTest) { - if (executable instanceof Constructor) { - throw new ParameterResolutionException( - "When a test class is annotated with @HelidonTest(resetPerMethod=true), constructor must not have " - + "parameters."); - } - } else { - // we need to start container before the test class is instantiated, to honor @BeforeAll that - // creates a custom MP config - if (container == null) { - startContainer(classLevelBeans, classLevelExtensions, classLevelDisableDiscovery); - } - } - - Class paramType = parameterContext.getParameter().getType(); - - if (executable instanceof Constructor) { - return !container.select(paramType).isUnsatisfied(); - } else if (executable instanceof Method) { - if (paramType.equals(SeContainer.class)) { - return true; - } - if (paramType.equals(WebTarget.class)) { - return true; - } - } - - return false; + return supplyChecked(ctx, () -> { + Store store = store(ctx, ctx.getRequiredTestMethod()); + HelidonTestContainerImpl container = requiredContainer(store); + return container.initFailed() ? null : container.resolveInstance(pc.getParameter().getType()); }); } - @Override - public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) - throws ParameterResolutionException { - - return super.supplyChecked(extensionContext, () -> { - Executable executable = parameterContext.getParameter().getDeclaringExecutable(); - Class paramType = parameterContext.getParameter().getType(); + private void invoke(Invocation invocation, + ReflectiveInvocationContext ic, + ExtensionContext context) throws Throwable { - if (executable instanceof Method) { - if (paramType.equals(SeContainer.class)) { - return container; - } - if (paramType.equals(WebTarget.class)) { - return container.select(WebTarget.class).get(); - } - } - // we return null, as construction of the object is done by CDI - // for primitive types we must return appropriate primitive default - if (paramType.isPrimitive()) { - // a hack to get to default value of a primitive type - return Array.get(Array.newInstance(paramType, 1), 0); + runChecked(context, () -> { + Store methodStore = store(context, context.getRequiredTestMethod()); + HelidonTestContainerImpl container = requiredContainer(methodStore); + if (container.initFailed()) { + invocation.skip(); } else { - return null; + // proxy handler uses class context + // hence we use a class context store specific to the test method + ExtensionContext classContext = classContext(context); + Store store = store(classContext, ic.getExecutable()); + store.put("container", container); + invocation.proceed(); } }); } - private void callAfterStop() { - List toInvoke = new ArrayList<>(); - - Method[] methods = testClass.getMethods(); - for (Method method : methods) { - AfterStop annotation = method.getAnnotation(AfterStop.class); - if (annotation != null) { - if (method.getParameterCount() != 0) { - throw new IllegalStateException("Method " + method + " is annotated with @AfterStop, but it has parameters"); - } - if (Modifier.isStatic(method.getModifiers())) { - method.setAccessible(true); - toInvoke.add(method); - } else { - throw new IllegalStateException("Method " + method + " is annotated with @AfterStop, but it is not static"); - } - } - } - - for (Method method : toInvoke) { - try { - method.invoke(testClass); - } catch (Exception e) { - throw new IllegalStateException("Failed to invoke method: " + method, e); - } - } + private static HelidonTestContainerImpl container(Store store) { + return storeLookup(store, "container", HelidonTestContainerImpl.class) + .orElse(null); } - // this is not registered as a bean - we manually register an instance - @SuppressWarnings("CdiManagedBeanInconsistencyInspection") - private static class AddBeansExtension implements Extension { - private final Class testClass; - private final List addBeans; - - private final HashMap socketAnnotations = new HashMap<>(); - - private AddBeansExtension(Class testClass, List addBeans) { - this.testClass = testClass; - this.addBeans = addBeans; - } - - void processSocketInjectionPoints(@Observes ProcessInjectionPoint event) { - InjectionPoint injectionPoint = event.getInjectionPoint(); - Set qualifiers = injectionPoint.getQualifiers(); - for (Annotation qualifier : qualifiers) { - if (qualifier.annotationType().equals(Socket.class)) { - String value = ((Socket) qualifier).value(); - socketAnnotations.put(value, qualifier); - break; - } - } - - } - - void registerOtherBeans(@Observes AfterBeanDiscovery event) { - - Client client = ClientBuilder.newClient(); - - //register for all named Ports - socketAnnotations.forEach((namedPort, qualifier) -> { - - event.addBean() - .addType(WebTarget.class) - .scope(ApplicationScoped.class) - .qualifiers(qualifier) - .createWith(context -> getWebTarget(client, namedPort)); - }); - - event.addBean() - .addType(jakarta.ws.rs.client.WebTarget.class) - .scope(ApplicationScoped.class) - .createWith(context -> getWebTarget(client, "@default")); - - } - - @SuppressWarnings("unchecked") - private static WebTarget getWebTarget(Client client, String namedPort) { - try { - Class extClass = (Class) Class - .forName("io.helidon.microprofile.server.ServerCdiExtension"); - Extension extension = CDI.current().getBeanManager().getExtension(extClass); - Method m = extension.getClass().getMethod("port", String.class); - int port = (int) m.invoke(extension, new Object[]{namedPort}); - String uri = "http://localhost:" + port; - return client.target(uri); - } catch (ReflectiveOperationException e) { - return client.target("http://localhost:7001"); - } - } - - void registerAddedBeans(@Observes BeforeBeanDiscovery event) { - event.addAnnotatedType(testClass, "junit-" + testClass.getName()) - .add(ApplicationScoped.Literal.INSTANCE); - - for (AddBean addBean : addBeans) { - Annotation scope; - Class definedScope = addBean.scope(); - - scope = BEAN_DEFINING.get(definedScope); - - if (scope == null) { - throw new IllegalStateException( - "Only on of " + BEAN_DEFINING.keySet() + " scopes are allowed in tests. Scope " - + definedScope.getName() + " is not allowed for bean " + addBean.value().getName()); - } - - AnnotatedTypeConfigurator configurator = event - .addAnnotatedType(addBean.value(), "junit-" + addBean.value().getName()); - if (!hasBda(addBean.value())) { - configurator.add(scope); - } - } - } - - private boolean hasBda(Class value) { - // does it have bean defining annotation? - for (Class aClass : BEAN_DEFINING.keySet()) { - if (value.getAnnotation(aClass) != null) { - return true; - } - } - - return false; - } - - } - - private static final class ConfigMeta { - private final Map additionalKeys = new HashMap<>(); - private final List additionalSources = new ArrayList<>(); - private String type; - private String block; - private boolean useExisting; - private String profile; - - private ConfigMeta() { - // to allow SeContainerInitializer (forbidden by default because of native image) - additionalKeys.put("mp.initializer.allow", "true"); - additionalKeys.put("mp.initializer.no-warn", "true"); - // to run on random port - additionalKeys.put("server.port", "0"); - // higher ordinal then all the defaults, system props and environment variables - additionalKeys.putIfAbsent(ConfigSource.CONFIG_ORDINAL, "1000"); - // profile - additionalKeys.put("mp.config.profile", "test"); - } - - private void addConfig(AddConfig[] configs) { - for (AddConfig config : configs) { - additionalKeys.put(config.key(), config.value()); - } - } - - private void configuration(Configuration config) { - if (config == null) { - return; - } - useExisting = config.useExisting(); - profile = config.profile(); - additionalSources.addAll(List.of(config.configSources())); - //set additional key for profile - additionalKeys.put("mp.config.profile", profile); - } - - private void addConfigBlock(AddConfigBlock config) { - if (config == null) { - return; - } - this.type = config.type(); - this.block = config.value(); - } - - 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; - } - } - - - /** - * Add WeldRequestScope. Used with {@code AddJaxRs}. - */ - private static final class WeldRequestScopeLiteral extends AnnotationLiteral implements AddBean { - - static final WeldRequestScopeLiteral INSTANCE = new WeldRequestScopeLiteral(); - - @Serial - private static final long serialVersionUID = 1L; - - @Override - public Class value() { - return org.glassfish.jersey.weld.se.WeldRequestScope.class; - } - - @Override - public Class scope() { - return RequestScoped.class; - } - } - - /** - * Add ProcessAllAnnotatedTypes. Used with {@code AddJaxRs}. - */ - private static final class ProcessAllAnnotatedTypesLiteral extends AnnotationLiteral implements AddExtension { - - static final ProcessAllAnnotatedTypesLiteral INSTANCE = new ProcessAllAnnotatedTypesLiteral(); - - @Serial - private static final long serialVersionUID = 1L; - - @Override - public Class value() { - return org.glassfish.jersey.ext.cdi1x.internal.ProcessAllAnnotatedTypes.class; - } + private static HelidonTestContainerImpl requiredContainer(Store store) { + return storeLookup(store, "container", HelidonTestContainerImpl.class) + .orElseThrow(() -> new IllegalStateException("Container not set")); } - /** - * Add ServerCdiExtension. Used with {@code AddJaxRs}. - */ - private static final class ServerCdiExtensionLiteral extends AnnotationLiteral implements AddExtension { - - static final ServerCdiExtensionLiteral INSTANCE = new ServerCdiExtensionLiteral(); - - @Serial - private static final long serialVersionUID = 1L; - - @Override - public Class value() { - return ServerCdiExtension.class; + private static ExtensionContext classContext(ExtensionContext context) { + ExtensionContext c = context; + while (!c.getElement().map(Class.class::isInstance).orElse(false)) { + c = c.getParent().orElseThrow(); } + return c; } - - /** - * Add WeldRequestScope. Used with {@code AddJaxRs}. - */ - private static final class JaxRsCdiExtensionLiteral extends AnnotationLiteral implements AddExtension { - - static final JaxRsCdiExtensionLiteral INSTANCE = new JaxRsCdiExtensionLiteral(); - - @Serial - private static final long serialVersionUID = 1L; - - @Override - public Class value() { - return JaxRsCdiExtension.class; - } - } - - /** - * Add CdiComponentProvider. Used with {@code AddJaxRs}. - */ - private static final class CdiComponentProviderLiteral extends AnnotationLiteral implements AddExtension { - - static final CdiComponentProviderLiteral INSTANCE = new CdiComponentProviderLiteral(); - - @Serial - private static final long serialVersionUID = 1L; - - @Override - public Class value() { - return CdiComponentProvider.class; - } - } - } diff --git a/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/HelidonTest.java b/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/HelidonTest.java index cb85395b11a..481b44a57fd 100644 --- a/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/HelidonTest.java +++ b/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/HelidonTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2024 Oracle and/or its affiliates. + * Copyright (c) 2020, 2025 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. @@ -21,37 +21,36 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestMethodOrder; import org.junit.jupiter.api.extension.ExtendWith; import static io.helidon.common.testing.virtualthreads.PinningRecorder.DEFAULT_THRESHOLD; /** - * An annotation making this test class a CDI bean with support for injection. + * A shorthand to use {@link HelidonJunitExtension} with additional settings. *

- * There is no need to provide {@code beans.xml} (actually it is not recommended, as it would combine beans - * from all tests), instead use {@link io.helidon.microprofile.testing.junit5.AddBean}, - * {@link io.helidon.microprofile.testing.junit5.AddExtension}, - * and {@link io.helidon.microprofile.testing.junit5.AddConfig} - * annotations to control the shape of the container. - *

- * To disable automated bean and extension discovery, annotate the class with - * {@link io.helidon.microprofile.testing.junit5.DisableDiscovery}. + * Sets the following defaults: + *

    + *
  • lifecycle: {@link TestInstance.Lifecycle#PER_CLASS}
  • + *
  • method order: {@link HelidonImplicitResetOrderer}
  • + *
+ * + * @see HelidonJunitExtension */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @ExtendWith(HelidonJunitExtension.class) +@TestMethodOrder(HelidonImplicitResetOrderer.class) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) @Inherited public @interface HelidonTest { /** - * By default, CDI container is created once before the class is initialized and shut down - * after. All test methods run within the same container. - * - * If this is set to {@code true}, a container is created per test method invocation. - * This restricts the test in the following way: - * 1. No injection into fields - * 2. No injection into constructor + * Forces the CDI container to be initialized and shutdown for each test method. + *

+ * The value of {@link org.junit.jupiter.api.TestInstance TestInstance} is ignored. * - * @return whether to reset container per test method + * @return whether to reset per test method */ boolean resetPerTest() default false; diff --git a/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/HelidonTestContainerImpl.java b/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/HelidonTestContainerImpl.java new file mode 100644 index 00000000000..19ba8c68406 --- /dev/null +++ b/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/HelidonTestContainerImpl.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2025 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.testing.junit5; + +import io.helidon.microprofile.testing.HelidonTestContainer; +import io.helidon.microprofile.testing.HelidonTestInfo; +import io.helidon.microprofile.testing.HelidonTestScope; + +import org.junit.jupiter.api.extension.ExtensionContext.Store.CloseableResource; + +/** + * Closeable test container. + */ +final class HelidonTestContainerImpl extends HelidonTestContainer implements CloseableResource { + + HelidonTestContainerImpl(HelidonTestInfo testInfo, HelidonTestScope testScope) { + super(testInfo, testScope, HelidonTestExtensionImpl::new); + } + + @Override + public void close() { + super.close(); + } +} diff --git a/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/HelidonTestDescriptorImpl.java b/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/HelidonTestDescriptorImpl.java new file mode 100644 index 00000000000..8d774e6203e --- /dev/null +++ b/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/HelidonTestDescriptorImpl.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2025 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.testing.junit5; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.util.List; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Stream; + +import io.helidon.microprofile.testing.HelidonTestDescriptorBase; + +import static io.helidon.microprofile.testing.Proxies.mirror; + +/** + * Base descriptor implementation that supports the deprecated annotations. + */ +@SuppressWarnings("deprecation") +class HelidonTestDescriptorImpl extends HelidonTestDescriptorBase { + + HelidonTestDescriptorImpl(T element) { + super(element); + } + + @Override + protected List lookupAddBeans() { + return lookup(io.helidon.microprofile.testing.AddBean.class, super.lookupAddBeans().stream(), + AddBean.class, AddBeans.class, AddBeans::value).toList(); + } + + @Override + protected List lookupAddConfigs() { + return lookup(io.helidon.microprofile.testing.AddConfig.class, super.lookupAddConfigs().stream(), + AddConfig.class, AddConfigs.class, AddConfigs::value).toList(); + } + + @Override + protected List lookupAddConfigBlocks() { + return Stream.concat(super.lookupAddConfigBlocks().stream(), annotations(AddConfigBlock.class) + .map(a -> mirror(io.helidon.microprofile.testing.AddConfigBlock.class, a))) + .toList(); + } + + @Override + protected List lookupAddExtensions() { + return lookup(io.helidon.microprofile.testing.AddExtension.class, super.lookupAddExtensions().stream(), + AddExtension.class, AddExtensions.class, AddExtensions::value).toList(); + } + + @Override + protected Optional lookupConfiguration() { + return super.lookupConfiguration().or(() -> annotations(Configuration.class) + .map(a -> mirror(io.helidon.microprofile.testing.Configuration.class, a)) + .findFirst()); + } + + @Override + protected boolean lookupAddJaxRs() { + return super.lookupAddJaxRs() || annotations(AddJaxRs.class) + .findFirst() + .isPresent(); + } + + @Override + protected boolean lookupDisableDiscovery() { + return super.lookupDisableDiscovery() || annotations(DisableDiscovery.class) + .findFirst() + .map(DisableDiscovery::value) + .orElse(false); + } + + @Override + protected boolean lookupResetPerTest() { + return annotations(HelidonTest.class) + .findFirst() + .map(HelidonTest::resetPerTest) + .orElse(false); + } + + @Override + protected boolean lookupPinningDetection() { + return annotations(HelidonTest.class) + .findFirst() + .map(HelidonTest::pinningDetection) + .orElse(false); + } + + @Override + public long pinningThreshold() { + return annotations(HelidonTest.class) + .findFirst() + .map(HelidonTest::pinningThreshold) + .orElse(20L); + } + + private Stream lookup(Class tType, + Stream initial, + Class aType, + Class cType, + Function function) { + + return Stream.concat(initial, annotations(aType, cType, function) + .map(a -> mirror(tType, a))); + } +} diff --git a/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/HelidonTestExtensionImpl.java b/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/HelidonTestExtensionImpl.java new file mode 100644 index 00000000000..c6ce0134c22 --- /dev/null +++ b/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/HelidonTestExtensionImpl.java @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2025 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.testing.junit5; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import io.helidon.microprofile.testing.HelidonTestExtension; +import io.helidon.microprofile.testing.HelidonTestInfo; +import io.helidon.microprofile.testing.HelidonTestScope; + +import static io.helidon.microprofile.testing.Proxies.mirror; + +/** + * An implementation of {@link HelidonTestExtension} that supports the deprecated annotations. + */ +@SuppressWarnings("deprecation") +final class HelidonTestExtensionImpl extends HelidonTestExtension { + + private static final Set> TYPE_ANNOTATION_TYPES = Set.of( + AddConfig.class, + AddConfigs.class, + AddConfigBlock.class, + Configuration.class); + + private static final Set> PARAMETER_ANNOTATION_TYPES = Set.of( + Socket.class); + + private static final Set> FIELD_ANNOTATION_TYPES = Set.of( + Socket.class); + + private static final Set> METHOD_ANNOTATION_TYPES = Set.of( + AddConfig.class, + AddConfigs.class, + AddConfigBlock.class, + AfterStop.class, + Configuration.class); + + private final Set> typeAnnotationTypes; + private final Set> parameterAnnotationTypes; + private final Set> fieldAnnotationTypes; + private final Set> methodAnnotationTypes; + + HelidonTestExtensionImpl(HelidonTestInfo testInfo, HelidonTestScope testScope) { + super(testInfo, testScope); + this.typeAnnotationTypes = concat(super.typeAnnotationTypes(), TYPE_ANNOTATION_TYPES); + this.parameterAnnotationTypes = concat(super.parameterAnnotationTypes(), PARAMETER_ANNOTATION_TYPES); + this.fieldAnnotationTypes = concat(super.fieldAnnotationTypes(), FIELD_ANNOTATION_TYPES); + this.methodAnnotationTypes = concat(super.methodAnnotationTypes(), METHOD_ANNOTATION_TYPES); + } + + @Override + protected Set> typeAnnotationTypes() { + return typeAnnotationTypes; + } + + @Override + protected Set> parameterAnnotationTypes() { + return parameterAnnotationTypes; + } + + @Override + protected Set> fieldAnnotationTypes() { + return fieldAnnotationTypes; + } + + @Override + protected Set> methodAnnotationTypes() { + return methodAnnotationTypes; + } + + @Override + protected void processTypeAnnotation(Annotation annotation) { + switch (annotation) { + case Configuration e -> processConfiguration(e); + case AddConfig e -> processAddConfig(e); + case AddConfigs e -> processAddConfig(e.value()); + case AddConfigBlock e -> processAddConfigBlock(e); + default -> super.processTypeAnnotation(annotation); + } + } + + @Override + protected void processParameterAnnotation(Annotation annotation) { + if (annotation instanceof Socket s) { + processSocket(s, s.value()); + } else { + super.processParameterAnnotation(annotation); + } + } + + @Override + protected void processFieldAnnotation(Annotation annotation) { + if (annotation instanceof Socket s) { + processSocket(s, s.value()); + } else { + super.processFieldAnnotation(annotation); + } + } + + @Override + protected void processStaticMethodAnnotation(Annotation annotation, Method method) { + if (annotation instanceof AfterStop) { + processAfterStop(method); + } else { + super.processStaticMethodAnnotation(annotation, method); + } + } + + @Override + protected void processTestMethodAnnotation(Annotation annotation, Method method) { + switch (annotation) { + case Configuration e -> processConfiguration(e); + case AddConfig e -> processAddConfig(e); + case AddConfigs e -> processAddConfig(e.value()); + case AddConfigBlock e -> processAddConfigBlock(e); + default -> super.processTestMethodAnnotation(annotation, method); + } + } + + private void processConfiguration(Configuration annotation) { + processConfiguration(mirror(io.helidon.microprofile.testing.Configuration.class, annotation)); + } + + private void processAddConfigBlock(AddConfigBlock annotation) { + processAddConfigBlock(mirror(io.helidon.microprofile.testing.AddConfigBlock.class, annotation)); + } + + private void processAddConfig(AddConfig... annotations) { + for (AddConfig annotation : annotations) { + processAddConfig(mirror(io.helidon.microprofile.testing.AddConfig.class, annotation)); + } + } + + private static Set> concat(Set> set1, + Set> set2) { + + return Stream.concat(set1.stream(), set2.stream()).collect(Collectors.toSet()); + } +} diff --git a/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/Socket.java b/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/Socket.java index 9852617ae4d..2d81c90b2c0 100644 --- a/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/Socket.java +++ b/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/Socket.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2022, 2025 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. @@ -23,18 +23,53 @@ import jakarta.inject.Qualifier; /** - * Named socket Qualifier for {@code WebTarget}. + * CDI qualifier to inject a JAX-RS client or URI for a named socket. + *

+ * The supported types are: + *

+ *

+ * This annotation can be used on constructor parameters, or class fields. + * Test method parameter injection may be supported depending on the test framework integration. + *

+ * Also note that the default socket name is {@code "@default"}. + *

+ * E.g. constructor injection: + *

+ * class MyTest {
+ *     private final WebTarget target;
+ *
+ *     @Inject
+ *     MyTest(@Socket("@default") URI uri) {
+ *         target = ClientBuilder.newClient().target(uri);
+ *     }
+ * }
+ * 
+ *

+ * E.g. field injection: + *

+ * class MyTest {
+ *
+ *     @Inject // optional
+ *     @Socket("@default")
+ *     private WebTarget target;
+ * }
+ * 
+ * @deprecated Use {@link io.helidon.microprofile.testing.Socket} instead */ @Qualifier @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.FIELD, ElementType.PARAMETER}) +@Deprecated(since = "4.2.0") public @interface Socket { /** * Name of the socket. * - * @return String with the name of the Socket + * @return socket name */ String value(); - } diff --git a/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/package-info.java b/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/package-info.java index d299e74a35e..5e997c48de9 100644 --- a/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/package-info.java +++ b/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2023 Oracle and/or its affiliates. + * Copyright (c) 2020, 2025 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. @@ -15,8 +15,8 @@ */ /** - * JUnit5 extension to run CDI tests. + * JUnit5 extension to support Helidon MP testing. * - * @see io.helidon.microprofile.testing.junit5.HelidonTest + * @see io.helidon.microprofile.testing.junit5.HelidonJunitExtension */ package io.helidon.microprofile.testing.junit5; diff --git a/microprofile/testing/junit5/src/main/java/module-info.java b/microprofile/testing/junit5/src/main/java/module-info.java index c50332eadf8..b2088cfca0c 100644 --- a/microprofile/testing/junit5/src/main/java/module-info.java +++ b/microprofile/testing/junit5/src/main/java/module-info.java @@ -17,23 +17,13 @@ /** * JUnit5 extension module to run CDI tests. */ +@SuppressWarnings("JavaModuleNaming") module io.helidon.microprofile.testing.junit5 { - requires io.helidon.config.mp; - requires io.helidon.config.yaml.mp; - requires io.helidon.testing.junit5; - requires io.helidon.microprofile.cdi; - requires jakarta.inject; + requires transitive io.helidon.microprofile.testing; requires org.junit.jupiter.api; - - requires transitive jakarta.cdi; - requires transitive jakarta.ws.rs; - - requires static io.helidon.microprofile.server; - requires static jersey.cdi1x; - requires static jersey.weld2.se; - requires io.helidon.common.testing.vitualthreads; + requires io.helidon.testing.junit5; + requires io.helidon.logging.common; exports io.helidon.microprofile.testing.junit5; - } diff --git a/microprofile/testing/mocking/src/main/java/io/helidon/microprofile/testing/mocking/MockBeansCdiExtension.java b/microprofile/testing/mocking/src/main/java/io/helidon/microprofile/testing/mocking/MockBeansCdiExtension.java index 855c9ac2d89..495eb21b188 100644 --- a/microprofile/testing/mocking/src/main/java/io/helidon/microprofile/testing/mocking/MockBeansCdiExtension.java +++ b/microprofile/testing/mocking/src/main/java/io/helidon/microprofile/testing/mocking/MockBeansCdiExtension.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 Oracle and/or its affiliates. + * Copyright (c) 2024, 2025 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. @@ -38,7 +38,7 @@ import org.mockito.Mockito; /** - * CDI extension for Mocking implementation. + * CDI extension that supports {@link MockBean}. */ public class MockBeansCdiExtension implements Extension { diff --git a/microprofile/testing/pom.xml b/microprofile/testing/pom.xml index e89c6692c9b..75e5dbcdab5 100644 --- a/microprofile/testing/pom.xml +++ b/microprofile/testing/pom.xml @@ -32,6 +32,7 @@ Helidon Microprofile Testing Project + testing junit5 testng mocking diff --git a/microprofile/testing/testing/pom.xml b/microprofile/testing/testing/pom.xml new file mode 100644 index 00000000000..2f8a3b359ac --- /dev/null +++ b/microprofile/testing/testing/pom.xml @@ -0,0 +1,73 @@ + + + + + 4.0.0 + + io.helidon.microprofile.testing + helidon-microprofile-testing-project + 4.2.0-SNAPSHOT + + helidon-microprofile-testing + Helidon Microprofile Testing + + + + io.helidon.common.testing + helidon-common-testing-virtual-threads + + + io.helidon.microprofile.server + helidon-microprofile-server + true + + + io.helidon.microprofile.cdi + helidon-microprofile-cdi + provided + + + org.glassfish.jersey.ext.cdi + jersey-weld2-se + true + + + net.bytebuddy + byte-buddy + + + io.helidon.jersey + helidon-jersey-client + + + io.helidon.config + helidon-config-yaml-mp + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + diff --git a/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/AddBean.java b/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/AddBean.java new file mode 100644 index 00000000000..48e40d0cfba --- /dev/null +++ b/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/AddBean.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2020, 2025 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.testing; + +import java.lang.annotation.Annotation; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Add a CDI bean to the container. + *

+ * This annotation can be repeated. + *

+ * If used on a method, the container will be reset regardless of the test lifecycle. + *

+ * The bean scope is defined as follows: + *

    + *
  • If a scope is set with {@link #value()}, it overrides any scope defined on the bean
  • + *
  • Otherwise, the scope defined on the bean is used
  • + *
  • If the bean does not define a scope, {@link jakarta.enterprise.context.ApplicationScoped ApplicationScoped} + * is used
  • + *
+ */ +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.METHOD}) +@Repeatable(AddBeans.class) +public @interface AddBean { + /** + * The bean class. + * + * @return bean class + */ + Class value(); + + /** + * Override the bean scope. + * + * @return scope class + */ + Class scope() default Annotation.class; +} diff --git a/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/AddBeans.java b/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/AddBeans.java new file mode 100644 index 00000000000..3929d7c6c6e --- /dev/null +++ b/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/AddBeans.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2022, 2025 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.testing; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * A repeatable container for {@link AddBean}. + *

+ * This annotation is optional, you can instead repeat {@link AddBean}. + *

+ * E.g. + *

+ * @AddBean(FooBean.class)
+ * @AddBean(BarBean.class)
+ * class MyTest {
+ * }
+ * 
+ */ +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.METHOD}) +public @interface AddBeans { + /** + * Get the contained annotations. + * + * @return annotations + */ + AddBean[] value(); +} diff --git a/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/AddConfig.java b/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/AddConfig.java new file mode 100644 index 00000000000..70c1343cfc7 --- /dev/null +++ b/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/AddConfig.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2022, 2025 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.testing; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Add a configuration key/value pair to the {@link Configuration#useExisting() synthetic test configuration}. + *

+ * This annotation can be repeated. + *

+ * If used on a method, the container will be reset regardless of the test lifecycle. + * + * @see io.helidon.microprofile.testing.AddConfigs + * @see io.helidon.microprofile.testing.AddConfigBlock + * @see io.helidon.microprofile.testing.Configuration + */ +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.METHOD}) +@Repeatable(AddConfigs.class) +public @interface AddConfig { + /** + * Configuration property key. + * + * @return key + */ + String key(); + + /** + * Configuration property value. + * + * @return value + */ + String value(); +} diff --git a/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/AddConfigBlock.java b/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/AddConfigBlock.java new file mode 100644 index 00000000000..2c6c21649a1 --- /dev/null +++ b/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/AddConfigBlock.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2025 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.testing; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Add a configuration fragment to the {@link Configuration#useExisting() synthetic test configuration}. + *

+ * This annotation can be repeated. + *

+ * If used on a method, the container will be reset regardless of the test lifecycle. + * + * @see AddConfig + * @see AddConfigs + * @see Configuration + */ +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.METHOD}) +@Repeatable(AddConfigBlocks.class) +public @interface AddConfigBlock { + /** + * Specifies the configuration format. + *

+ * The default format is 'properties' + * + * @return the supported type + */ + String type() default "properties"; + + /** + * Configuration fragment. + * + * @return fragment + */ + String value(); +} diff --git a/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/AddConfigBlocks.java b/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/AddConfigBlocks.java new file mode 100644 index 00000000000..bbe8931331b --- /dev/null +++ b/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/AddConfigBlocks.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2022, 2025 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.testing; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * A repeatable container for {@link AddConfigBlock}. + *

+ * This annotation is optional, you can instead repeat {@link AddConfigBlock}. + *

+ * E.g. + *

+ * @AddConfigBlock(type = "yaml", value = """
+ *     foo1:
+ *       bar: "value1"
+ * """)
+ * @AddConfigBlock(type = "yaml", value = """
+ *     foo2:
+ *       bar: "value2"
+ * """)
+ * class MyTest {
+ * }
+ * 
+ * + * @see AddConfig + * @see io.helidon.microprofile.testing.AddConfigBlocks + * @see Configuration + */ +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.METHOD}) +public @interface AddConfigBlocks { + /** + * Get the contained annotations. + * + * @return annotations + */ + AddConfigBlock[] value(); +} diff --git a/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/AddConfigs.java b/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/AddConfigs.java new file mode 100644 index 00000000000..442bffac7d3 --- /dev/null +++ b/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/AddConfigs.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2022, 2025 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.testing; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * A repeatable container for {@link AddConfig}. + *

+ * This annotation is optional, you can instead repeat {@link AddConfig}. + *

+ * E.g. + *

+ * @AddConfig(key="foo", value="1")
+ * @AddConfig(key="bar", value="2")
+ * class MyTest {
+ * }
+ * 
+ * + * @see AddConfig + * @see io.helidon.microprofile.testing.AddConfigs + * @see Configuration + */ +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.METHOD}) +public @interface AddConfigs { + /** + * Get the contained annotations. + * + * @return annotations + */ + AddConfig[] value(); +} diff --git a/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/AddExtension.java b/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/AddExtension.java new file mode 100644 index 00000000000..271117a0fe1 --- /dev/null +++ b/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/AddExtension.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2020, 2025 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.testing; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import jakarta.enterprise.inject.spi.Extension; + +/** + * Add a CDI extension to the container. + *

+ * This annotation can be repeated. + *

+ * If used on a method, the container will be reset regardless of the test lifecycle. + */ +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.METHOD}) +@Repeatable(AddExtensions.class) +public @interface AddExtension { + /** + * Class of the extension to add. The class must be public. + * + * @return extension class. + */ + Class value(); +} diff --git a/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/AddExtensions.java b/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/AddExtensions.java new file mode 100644 index 00000000000..87c9c63f55c --- /dev/null +++ b/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/AddExtensions.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2022, 2025 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.testing; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * A repeatable container for {@link AddExtension}. + *

+ * This annotation is optional, you can instead repeat {@link AddExtension}. + *

+ * E.g. + *

+ * @AddExtension(FooExtension.class)
+ * @AddExtension(BarExtension.class)
+ * class MyTest {
+ * }
+ * 
+ */ +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.METHOD}) +public @interface AddExtensions { + /** + * Get the contained annotations. + * + * @return annotations + */ + AddExtension[] value(); +} diff --git a/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/AddJaxRs.java b/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/AddJaxRs.java new file mode 100644 index 00000000000..fd9ab0a1cbe --- /dev/null +++ b/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/AddJaxRs.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2023, 2025 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.testing; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import io.helidon.microprofile.server.JaxRsCdiExtension; +import io.helidon.microprofile.server.ServerCdiExtension; + +import org.glassfish.jersey.ext.cdi1x.internal.CdiComponentProvider; +import org.glassfish.jersey.ext.cdi1x.internal.ProcessAllAnnotatedTypes; +import org.glassfish.jersey.weld.se.WeldRequestScope; + +/** + * Add JAX-RS (Jersey) support. + *

+ * If used on a method, the container will be reset regardless of the test lifecycle. + */ +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.METHOD}) +@AddExtension(ProcessAllAnnotatedTypes.class) +@AddExtension(ServerCdiExtension.class) +@AddExtension(JaxRsCdiExtension.class) +@AddExtension(CdiComponentProvider.class) +@AddBean(WeldRequestScope.class) +public @interface AddJaxRs { +} diff --git a/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/AfterStop.java b/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/AfterStop.java new file mode 100644 index 00000000000..259c27094dd --- /dev/null +++ b/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/AfterStop.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023, 2025 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.testing; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Mark a static method to be executed after the container is stopped. + *

+ * E.g. + *

+ * @AfterStop
+ * static void afterStop() {
+ *     // ...
+ * }
+ */ +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface AfterStop { +} diff --git a/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/Configuration.java b/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/Configuration.java new file mode 100644 index 00000000000..ada27e49d71 --- /dev/null +++ b/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/Configuration.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2020, 2025 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.testing; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * General setting for the test configuration. + *

+ * If used on a method, the container will be reset regardless of the test lifecycle. + * + * @see AddConfig + * @see io.helidon.microprofile.testing.AddConfigs + * @see io.helidon.microprofile.testing.AddConfigBlock + */ +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.METHOD}) +public @interface Configuration { + /** + * If set to {@code false}, the synthetic test configuration is used. + *

+ * The synthetic test configuration is expressed with the following: + *

    + *
  • {@link #configSources()}
  • + *
  • {@link #profile()}
  • + *
  • {@link AddConfig}
  • + *
  • {@link AddConfigs}
  • + *
  • {@link AddConfigBlock}
  • + *
+ *

+ * If set to {@code true}, only the existing (or default) MicroProfile configuration is used as-is + * and the annotations listed previously are ignored. + *

+ * You can use {@link org.eclipse.microprofile.config.spi.ConfigProviderResolver ConfigProviderResolver} to define + * the configuration programmatically before the CDI container starts. + * + * @return whether to use existing (or default) configuration + */ + boolean useExisting() default false; + + /** + * Class-path resources to add as config sources to the synthetic test configuration. + * + * @return config sources + */ + String[] configSources() default {}; + + /** + * Configuration profile. + *

+ * The default profile is 'test' + * + * @return profile + */ + String profile() default "test"; +} diff --git a/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/DisableDiscovery.java b/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/DisableDiscovery.java new file mode 100644 index 00000000000..cb69b97bca1 --- /dev/null +++ b/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/DisableDiscovery.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2022, 2025 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.testing; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Disables CDI discovery. + *

+ * If discovery is desired, do not annotate test class with this annotation. + *

+ * If used on a method, the container will be reset regardless of the test lifecycle. + *

+ * When disabling discovery, you are responsible for adding the beans and extensions needed to activate the features you need. + * You can use the following annotations to do that: + *

    + *
  • {@link AddBean} to add CDI beans
  • + *
  • {@link AddExtension} to add CDI extensions
  • + *
  • {@link AddJaxRs} a shorthand to add JAX-RS (Jersey)
  • + *
+ *

+ * See also the following "core" CDI extensions: + *

    + *
  • {@link io.helidon.microprofile.server.ServerCdiExtension ServerCdiExtension} optional if using {@link AddJaxRs}
  • + *
  • {@link io.helidon.microprofile.server.JaxRsCdiExtension JaxRsCdiExtension} optional if using {@link AddJaxRs}
  • + *
  • {@link io.helidon.microprofile.config.ConfigCdiExtension ConfigCdiExtension}
  • + *
+ */ +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.METHOD}) +public @interface DisableDiscovery { + /** + * By default, if you annotate a class or a method, discovery gets disabled. + * If you want to override configuration on method to differ from class, you + * can configure the value to {@code false}, effectively enabling discovery. + * + * @return whether to disable discovery ({@code true}), or enable it ({@code false}). If this + * annotation is not present, discovery is enabled + */ + boolean value() default true; +} diff --git a/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/HelidonTestConfig.java b/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/HelidonTestConfig.java new file mode 100644 index 00000000000..32a6639a600 --- /dev/null +++ b/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/HelidonTestConfig.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2025 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.testing; + +import java.util.Map; + +import io.helidon.config.mp.MpConfigSources; + +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.spi.ConfigProviderResolver; + +/** + * Helidon test configuration. + *

+ * A config delegate that serves one of three config instances: + *

    + *
  • bootstrapping configuration
  • + *
  • synthetic configuration
  • + *
  • original configuration
  • + *
+ *

+ * This mechanism removes the need to define bootstrapping configuration when using {@link Configuration#useExisting()}. + *

+ * This mechanism also provides a way to update the synthetic configuration at a later stage using CDI. + */ +class HelidonTestConfig extends HelidonTestConfigDelegate { + + private final HelidonTestConfigSynthetic syntheticConfig; + private final Config originalConfig; + private volatile Config delegate; + + HelidonTestConfig(HelidonTestInfo testInfo) { + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + ConfigProviderResolver resolver = ConfigProviderResolver.instance(); + + originalConfig = resolver.getConfig(cl); + + // release original config + resolver.releaseConfig(originalConfig); + + // start with bootstrap config + delegate = resolver.getBuilder() + .withSources(MpConfigSources.create(Map.of( + "mp.initializer.allow", "true", + "mp.initializer.no-warn", "true"))) + .build(); + + syntheticConfig = new HelidonTestConfigSynthetic(testInfo, this::refresh); + + // register delegate + resolver.registerConfig(this, cl); + } + + @Override + Config delegate() { + return delegate; + } + + /** + * Get the synthetic config. + * + * @return synthetic config + */ + HelidonTestConfigSynthetic synthetic() { + return syntheticConfig; + } + + /** + * Resolve the delegate. + */ + void resolve() { + if (syntheticConfig.useExisting()) { + delegate = originalConfig; + } else { + delegate = syntheticConfig; + } + } +} diff --git a/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/HelidonTestConfigDelegate.java b/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/HelidonTestConfigDelegate.java new file mode 100644 index 00000000000..82c40af8c3a --- /dev/null +++ b/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/HelidonTestConfigDelegate.java @@ -0,0 +1,239 @@ +/* + * Copyright (c) 2025 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.testing; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Stream; + +import io.helidon.common.GenericType; +import io.helidon.common.LazyValue; +import io.helidon.config.Config; +import io.helidon.config.ConfigMappingException; +import io.helidon.config.ConfigValue; +import io.helidon.config.MissingValueException; +import io.helidon.config.spi.ConfigMapper; +import io.helidon.config.spi.ConfigNode; +import io.helidon.config.spi.ConfigSource; +import io.helidon.config.spi.LazyConfigSource; + +/** + * Config delegate. + *

+ * Also implements a {@link Config Helidon Config} delegate backed by a {@link LazyConfigSource} + * to support "just in time" caching when using {@link io.helidon.config.Config Helidon Config}. + */ +abstract class HelidonTestConfigDelegate implements org.eclipse.microprofile.config.Config, Config { + + private final LazyValue hdelegate = LazyValue.create(this::delegate0); + private final Map> cache = new HashMap<>(); + + /* + * Get the MicroProfile config delegate. + * + * @return delegate + */ + abstract org.eclipse.microprofile.config.Config delegate(); + + @Override + public T getValue(String propertyName, Class propertyType) { + return delegate().getValue(propertyName, propertyType); + } + + @Override + public org.eclipse.microprofile.config.ConfigValue getConfigValue(String propertyName) { + return delegate().getConfigValue(propertyName); + } + + @Override + public List getValues(String propertyName, Class propertyType) { + return delegate().getValues(propertyName, propertyType); + } + + @Override + public Optional getOptionalValue(String propertyName, Class propertyType) { + return delegate().getOptionalValue(propertyName, propertyType); + } + + @Override + public Optional> getOptionalValues(String propertyName, Class propertyType) { + return delegate().getOptionalValues(propertyName, propertyType); + } + + @Override + public Iterable getPropertyNames() { + return delegate().getPropertyNames(); + } + + @Override + public Iterable getConfigSources() { + return delegate().getConfigSources(); + } + + @Override + public Optional> getConverter(Class forType) { + return delegate().getConverter(forType); + } + + @Override + public T unwrap(Class type) { + return delegate().unwrap(type); + } + + @Override + public Instant timestamp() { + return hdelegate.get().timestamp(); + } + + @Override + public Key key() { + return hdelegate.get().key(); + } + + @Override + public Config root() { + return hdelegate.get().root(); + } + + @Override + public Config get(Key key) { + return hdelegate.get().get(key); + } + + @Override + public Config detach() { + return hdelegate.get().detach(); + } + + @Override + public Type type() { + return hdelegate.get().type(); + } + + @Override + public boolean hasValue() { + return hdelegate.get().hasValue(); + } + + @Override + public Stream traverse(Predicate predicate) { + return hdelegate.get().traverse(predicate); + } + + @Override + public T convert(Class type, String value) throws ConfigMappingException { + return hdelegate.get().convert(type, value); + } + + @Override + public ConfigMapper mapper() { + return hdelegate.get().mapper(); + } + + @Override + public ConfigValue as(GenericType genericType) { + return hdelegate.get().as(genericType); + } + + @Override + public ConfigValue as(Class type) { + return hdelegate.get().as(type); + } + + @Override + public ConfigValue as(Function mapper) { + return hdelegate.get().as(mapper); + } + + @Override + public ConfigValue> asList(Class type) throws ConfigMappingException { + return hdelegate.get().asList(type); + } + + @Override + public ConfigValue> asList(Function mapper) throws ConfigMappingException { + return hdelegate.get().asList(mapper); + } + + @Override + public ConfigValue> asNodeList() throws ConfigMappingException { + return hdelegate.get().asNodeList(); + } + + @Override + public ConfigValue> asMap() throws MissingValueException { + return hdelegate.get().asMap(); + } + + /** + * Refresh the cached property names for the current delegate. + */ + void refresh() { + org.eclipse.microprofile.config.Config delegate = delegate(); + if (delegate != null) { + cache.computeIfPresent(delegate, (k, v) -> { + List names = new ArrayList<>(); + for (String name : k.getPropertyNames()) { + names.add(name); + } + return names; + }); + } + } + + private List propertyNames(org.eclipse.microprofile.config.Config config) { + List names = new ArrayList<>(); + for (String name : config.getPropertyNames()) { + names.add(name); + } + return names; + } + + private Config delegate0() { + return Config.just((ConfigSource & LazyConfigSource) key -> { + org.eclipse.microprofile.config.Config delegate = delegate(); + if (delegate != null) { + String value = delegate.getConfigValue(key).getValue(); + if (value != null) { + // simple value + return Optional.of(ConfigNode.ValueNode.create(value)); + } + // complex value + List propertyNames = cache.computeIfAbsent(delegate, this::propertyNames); + ConfigNode.ObjectNode.Builder builder = ConfigNode.ObjectNode.builder(); + boolean hasEntries = false; + for (String name : propertyNames) { + if (name.startsWith(key + ".")) { + String k = name.substring(key.length() + 1); + String v = delegate.getConfigValue(name).getValue(); + builder.addValue(k, v); + hasEntries = true; + } + } + if (hasEntries) { + return Optional.of(builder.build()); + } + } + return Optional.empty(); + }); + } +} diff --git a/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/HelidonTestConfigSynthetic.java b/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/HelidonTestConfigSynthetic.java new file mode 100644 index 00000000000..94da0c4d540 --- /dev/null +++ b/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/HelidonTestConfigSynthetic.java @@ -0,0 +1,192 @@ +/* + * Copyright (c) 2025 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.testing; + +import java.io.IOException; +import java.io.StringReader; +import java.io.UncheckedIOException; +import java.lang.reflect.Method; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.locks.ReentrantLock; + +import io.helidon.config.mp.MpConfigSources; + +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.spi.ConfigBuilder; +import org.eclipse.microprofile.config.spi.ConfigProviderResolver; +import org.eclipse.microprofile.config.spi.ConfigSource; + +import static io.helidon.microprofile.testing.ReflectionHelper.invoke; +import static io.helidon.microprofile.testing.ReflectionHelper.requireStatic; + +/** + * The synthetic test configuration that is expressed with annotations. + *

+ * The delegate is initialized with the annotations extracted via {@link HelidonTestInfo}. + *

+ * The delegate is re-built when the definitions are updated via the {@code update} methods. + */ +class HelidonTestConfigSynthetic extends HelidonTestConfigDelegate { + + private final Map map = new HashMap<>(); + private final Map> blocks = new HashMap<>(); + private final Set methods = new HashSet<>(); + private final Set resources = new HashSet<>(); + private final HelidonTestInfo testInfo; + private final ReentrantLock lock = new ReentrantLock(); + private final Runnable onUpdate; + private boolean useExisting; + private Config config; + + HelidonTestConfigSynthetic(HelidonTestInfo testInfo, Runnable onUpdate) { + this.testInfo = testInfo; + this.onUpdate = onUpdate; + map.put(ConfigSource.CONFIG_ORDINAL, "1000"); + map.put("server.port", "0"); + map.put("mp.config.profile", "test"); + testInfo.addConfigs().forEach(this::update); + testInfo.addConfigBlocks().forEach(this::update); + testInfo.configuration().ifPresent(this::update); + } + + @Override + Config delegate() { + if (config == null) { + try { + lock.lock(); + if (config == null) { + config = buildConfig(); + onUpdate.run(); + } + } finally { + lock.unlock(); + } + } + return config; + } + + /** + * Update. + * + * @param annotation annotation + */ + void update(Configuration annotation) { + map.put("mp.config.profile", annotation.profile()); + useExisting = annotation.useExisting(); + List sources = List.of(annotation.configSources()); + if (!resources.containsAll(sources)) { + resources.addAll(sources); + config = null; + } + } + + /** + * Update. + * + * @param annotations annotations + */ + void update(AddConfig... annotations) { + for (AddConfig annotation : annotations) { + map.put(annotation.key(), annotation.value()); + } + } + + /** + * Update. + * + * @param annotations annotations + */ + void update(AddConfigBlock... annotations) { + for (AddConfigBlock annotation : annotations) { + if (blocks.computeIfAbsent(annotation.type(), t -> new HashSet<>()) + .add(annotation.value())) { + config = null; + } + } + } + + /** + * Update. + * + * @param method method + */ + void update(Method method) { + if (methods.add(requireStatic(method))) { + config = null; + } + } + + /** + * Get the effective value of {@link Configuration#useExisting()}. + * + * @return useExisting + */ + boolean useExisting() { + return useExisting; + } + + private Config buildConfig() { + List configSources = new ArrayList<>(); + configSources.add(MpConfigSources.create(testInfo.id(), map)); + blocks.forEach((type, values) -> { + for (String value : values) { + configSources.add(MpConfigSources.create(type, new StringReader(value))); + } + }); + for (Method m : methods) { + configSources.add(invoke(ConfigSource.class, requireStatic(m), null)); + } + for (String source : resources) { + String filename = source.trim(); + for (URL url : resources(filename)) { + String type = extension(filename); + configSources.add(MpConfigSources.create(type, url)); + } + } + ConfigBuilder builder = ConfigProviderResolver.instance() + .getBuilder() + .addDefaultSources() + .addDiscoveredSources() + .addDiscoveredConverters(); + configSources.forEach(builder::withSources); + return builder.build(); + } + + private static String extension(String filename) { + int idx = filename.lastIndexOf('.'); + return idx > -1 ? filename.substring(idx + 1) : "properties"; + } + + private static Collection resources(String name) { + try { + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + Map urls = new HashMap<>(); + cl.getResources(name).asIterator() + .forEachRemaining(u -> urls.put(u.toString(), u)); + return urls.values(); + } catch (IOException e) { + throw new UncheckedIOException(String.format( + "Failed to read '%s' from classpath", name), e); + } + } +} diff --git a/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/HelidonTestContainer.java b/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/HelidonTestContainer.java new file mode 100644 index 00000000000..06abc6fdb4b --- /dev/null +++ b/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/HelidonTestContainer.java @@ -0,0 +1,186 @@ +/* + * Copyright (c) 2025 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.testing; + +import java.lang.System.Logger.Level; +import java.time.Duration; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.BiFunction; + +import io.helidon.common.testing.virtualthreads.PinningRecorder; + +import jakarta.enterprise.inject.se.SeContainer; +import jakarta.enterprise.inject.se.SeContainerInitializer; + +import static io.helidon.microprofile.testing.ReflectionHelper.requirePublic; + +/** + * CDI container testing facade. + */ +public class HelidonTestContainer { + + /** + * Indicate that the container previously failed to initialize. + */ + public static final class InitializationFailed extends RuntimeException { + private InitializationFailed(RuntimeException error) { + super("Container initialization previously failed", error); + } + } + + private static final System.Logger LOGGER = System.getLogger(HelidonTestContainer.class.getName()); + private static final AtomicInteger NEXT_ID = new AtomicInteger(1); + + private final HelidonTestInfo testInfo; + private final HelidonTestScope testScope; + private final BiFunction, HelidonTestScope, HelidonTestExtension> extensionFactory; + private final AtomicBoolean closed = new AtomicBoolean(false); + private final ReentrantLock lock = new ReentrantLock(); + private final int id = NEXT_ID.getAndIncrement(); + private SeContainer container; + private PinningRecorder pinningRecorder; + private RuntimeException error; + + /** + * Create a new instance. + * + * @param testInfo test info + * @param testScope test scope + * @param extensionFactory extension factory + */ + public HelidonTestContainer(HelidonTestInfo testInfo, + HelidonTestScope testScope, + BiFunction, HelidonTestScope, HelidonTestExtension> extensionFactory) { + + this.testInfo = testInfo; + this.testScope = testScope; + this.extensionFactory = extensionFactory; + } + + /** + * Stop the container. + */ + public void close() { + if (container != null && closed.compareAndSet(false, true)) { + LOGGER.log(Level.DEBUG, "closing container id={0}", id); + container.close(); + if (pinningRecorder != null) { + pinningRecorder.close(); + } + } + } + + /** + * Indicate if the container is closed. + * + * @return {@code true} if closed, {@code false} otherwise + */ + public boolean closed() { + return closed.get(); + } + + /** + * Indicate if the container initializion failed. + * + * @return {@code true} if failed, {@code false} otherwise + */ + public boolean initFailed() { + return error != null; + } + + /** + * Resolve an unqualified bean of the given type. + * + * @param type type + * @param type + * @return resolved instance + * @throws InitializationFailed if the container previusly failed to + * start + */ + @SuppressWarnings("resource") + public T resolveInstance(Class type) throws InitializationFailed { + if (type.isAssignableFrom(SeContainer.class)) { + return type.cast(container()); + } + return container().select(type).get(); + } + + /** + * Test if the given type is supported for injection. + * + * @param type type + * @return {@code true} if supported, {@code false} otherwise + * @throws InitializationFailed if the container previusly failed to + * start + */ + @SuppressWarnings("resource") + public boolean isSupported(Class type) throws InitializationFailed { + if (type.isAssignableFrom(SeContainer.class)) { + return true; + } + return !container().select(type).isUnsatisfied(); + } + + private SeContainer container() { + if (error == null && container == null) { + try { + lock.lock(); + if (error == null && container == null) { + start(); + } + } catch (RuntimeException ex) { + error = ex; + throw ex; + } finally { + lock.unlock(); + } + } + if (error != null) { + throw new InitializationFailed(error); + } + return container; + } + + @SuppressWarnings("unchecked") + private void start() { + LOGGER.log(Level.DEBUG, "starting container\n{0}", this); + if (testInfo.pinningDetection()) { + pinningRecorder = PinningRecorder.create(); + pinningRecorder.record(Duration.ofMillis(testInfo.pinningThreshold())); + } + HelidonTestExtension testExtension = extensionFactory.apply(testInfo, testScope); + SeContainerInitializer initializer = SeContainerInitializer.newInstance(); + if (testInfo.disableDiscovery()) { + initializer.disableDiscovery(); + } + for (AddExtension extension : testInfo.addExtensions()) { + initializer.addExtensions(requirePublic(extension.value())); + } + initializer.addExtensions(testExtension); + container = initializer.initialize(); + } + + @Override + public String toString() { + return new PrettyPrinter() + .object(printer -> printer + .value("id", id) + .object("testInfo", PrettyPrinters.testInfo(testInfo))) + .toString(); + } +} diff --git a/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/HelidonTestDescriptor.java b/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/HelidonTestDescriptor.java new file mode 100644 index 00000000000..112e776e5c5 --- /dev/null +++ b/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/HelidonTestDescriptor.java @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2025 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.testing; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.util.List; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Stream; + +import jakarta.enterprise.inject.spi.Extension; + +import static io.helidon.common.testing.virtualthreads.PinningRecorder.DEFAULT_THRESHOLD; + +/** + * Describes annotations for a test class or method. + * + * @param element type + */ +public interface HelidonTestDescriptor { + + /** + * Get the annotated element. + * + * @return element + */ + T element(); + + /** + * Get the discovered value of {@code @HelidonTest(resetPerTest = true)}. + * + * @return {@code resetPerTest} value + */ + default boolean resetPerTest() { + return false; + } + + /** + * Get the discovered value of {@code @HelidonTest(pinningDetection = true)}. + * + * @return {@code pinningDetection} value + */ + default boolean pinningDetection() { + return false; + } + + /** + * Get the discovered value of {@code @HelidonTest(pinningThreshold = 50)}. + * + * @return {@code pinningThreshold} value + */ + default long pinningThreshold() { + return DEFAULT_THRESHOLD; + } + + /** + * Get the discovered {@link AddJaxRs} annotation. + * + * @return {@code true} if the annotation is present + */ + boolean addJaxRs(); + + /** + * Get the value of the discovered {@link DisableDiscovery} annotation. + * + * @return {@link DisableDiscovery#value()} or {@code false} if not found + */ + boolean disableDiscovery(); + + /** + * Get the discovered {@link AddExtension} annotations. + * + * @return annotations + */ + List addExtensions(); + + /** + * Get the discovered {@link AddBean} annotations. + * + * @return annotations + */ + List addBeans(); + + /** + * Get the discovered {@link Configuration} annotation. + * + * @return annotation + */ + Optional configuration(); + + /** + * Get the discovered {@link AddConfig} annotations. + * + * @return annotations + */ + List addConfigs(); + + /** + * Get the discovered {@link AddConfigBlock} annotations. + * + * @return annotations + */ + List addConfigBlocks(); + + /** + * Test if the given extension is configured. + * + * @param type extension type + * @return {@code true} if configured, {@code false} otherwise + */ + default boolean containsExtension(Class type) { + return addExtensions().stream() + .map(AddExtension::value) + .anyMatch(Predicate.isEqual(type)); + } + + /** + * Get annotations. + * + * @param aType annotation type + * @param cType annotation container type + * @param function function to inflate from container + * @param annotation type + * @param container type + * @return annotations + */ + Stream annotations(Class aType, + Class cType, + Function function); + + /** + * Get annotations. + * + * @param aType annotation type + * @param annotation type + * @return annotations + */ + Stream annotations(Class aType); + + /** + * Test if an annotation of the given type is found. + * + * @param aType annotation type + * @return {@code true} if found + */ + default boolean containsAnnotation(Class aType) { + return annotations(aType).findFirst().isPresent(); + } +} diff --git a/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/HelidonTestDescriptorBase.java b/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/HelidonTestDescriptorBase.java new file mode 100644 index 00000000000..b5ef6990ddc --- /dev/null +++ b/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/HelidonTestDescriptorBase.java @@ -0,0 +1,233 @@ +/* + * Copyright (c) 2025 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.testing; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Method; +import java.util.List; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Stream; + +import io.helidon.common.LazyValue; +import io.helidon.microprofile.testing.ReflectionHelper.Annotated; + +import static io.helidon.common.testing.virtualthreads.PinningRecorder.DEFAULT_THRESHOLD; +import static io.helidon.microprofile.testing.ReflectionHelper.annotated; +import static io.helidon.microprofile.testing.ReflectionHelper.filterAnnotations; + +/** + * Base implementation. + * + * @param annotated element type + */ +public abstract class HelidonTestDescriptorBase implements HelidonTestDescriptor { + + private final T element; + private final List> annotated; + private final LazyValue resetPerTest = LazyValue.create(this::lookupResetPerTest); + private final LazyValue pinningDetection = LazyValue.create(this::lookupPinningDetection); + private final LazyValue pinningThreshold = LazyValue.create(this::lookupPinningThreshold); + private final LazyValue> addExtensions = LazyValue.create(this::lookupAddExtensions); + private final LazyValue> addBeans = LazyValue.create(this::lookupAddBeans); + private final LazyValue addJaxRs = LazyValue.create(this::lookupAddJaxRs); + private final LazyValue disableDiscovery = LazyValue.create(this::lookupDisableDiscovery); + private final LazyValue> addConfigs = LazyValue.create(this::lookupAddConfigs); + private final LazyValue> addConfigBlocks = LazyValue.create(this::lookupAddConfigBlocks); + private final LazyValue> configuration = LazyValue.create(this::lookupConfiguration); + + /** + * Create a new instance. + * + * @param element element + */ + protected HelidonTestDescriptorBase(T element) { + this.element = element; + this.annotated = switch (element) { + case Class c -> annotated(c); + case Method m -> annotated(m); + default -> throw new IllegalArgumentException("Unsupported element: " + element); + }; + } + + @Override + public T element() { + return element; + } + + @Override + public boolean resetPerTest() { + return resetPerTest.get(); + } + + @Override + public boolean pinningDetection() { + return pinningDetection.get(); + } + + @Override + public long pinningThreshold() { + return pinningThreshold.get(); + } + + @Override + public List addExtensions() { + return addExtensions.get(); + } + + @Override + public List addBeans() { + return addBeans.get(); + } + + @Override + public boolean addJaxRs() { + return addJaxRs.get(); + } + + @Override + public boolean disableDiscovery() { + return disableDiscovery.get(); + } + + @Override + public Optional configuration() { + return configuration.get(); + } + + @Override + public List addConfigs() { + return addConfigs.get(); + } + + @Override + public List addConfigBlocks() { + return addConfigBlocks.get(); + } + + /** + * Lookup the value of {@code @HelidonTest(resetPerTest = true)}. + * + * @return {@code resetPerTest} value + */ + protected boolean lookupResetPerTest() { + return false; + } + + /** + * Lookup the value of {@code @HelidonTest(pinningDetection = true)}. + * + * @return {@code pinningDetection} value + */ + protected boolean lookupPinningDetection() { + return false; + } + + /** + * Lookup the value of {@code @HelidonTest(pinningThreshold = 50)}. + * + * @return {@code pinningThreshold} value + */ + protected long lookupPinningThreshold() { + return DEFAULT_THRESHOLD; + } + + /** + * Lookup the {@link AddExtension} annotations. + * + * @return annotations + */ + protected List lookupAddExtensions() { + return annotations(AddExtension.class, AddExtensions.class, AddExtensions::value) + .toList(); + } + + /** + * Lookup the {@link AddBean} annotations. + * + * @return annotations + */ + protected List lookupAddBeans() { + return annotations(AddBean.class, AddBeans.class, AddBeans::value) + .toList(); + } + + /** + * Lookup the {@link AddJaxRs} annotation. + * + * @return annotation + */ + protected boolean lookupAddJaxRs() { + return annotations(AddJaxRs.class) + .findFirst() + .isPresent(); + } + + /** + * Lookup the {@link DisableDiscovery} annotation. + * + * @return annotation + */ + protected boolean lookupDisableDiscovery() { + return annotations(DisableDiscovery.class) + .findFirst() + .map(DisableDiscovery::value) + .orElse(false); + } + + /** + * Lookup the {@link Configuration} annotation. + * + * @return annotation + */ + protected Optional lookupConfiguration() { + return annotations(Configuration.class) + .findFirst(); + } + + /** + * Lookup the {@link AddConfig} annotations. + * + * @return annotations + */ + protected List lookupAddConfigs() { + return annotations(AddConfig.class, AddConfigs.class, AddConfigs::value) + .toList(); + } + + /** + * Lookup the {@link AddConfigBlock} annotations. + * + * @return annotations + */ + protected List lookupAddConfigBlocks() { + return annotations(AddConfigBlock.class, AddConfigBlocks.class, AddConfigBlocks::value) + .toList(); + } + + @Override + public Stream annotations(Class aType, + Class cType, + Function function) { + return filterAnnotations(annotated, aType, cType, function); + } + + @Override + public Stream annotations(Class aType) { + return filterAnnotations(annotated, aType); + } +} diff --git a/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/HelidonTestDescriptorDelegate.java b/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/HelidonTestDescriptorDelegate.java new file mode 100644 index 00000000000..6fb7dbaa8c3 --- /dev/null +++ b/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/HelidonTestDescriptorDelegate.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2025 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.testing; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.util.List; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Stream; + +/** + * Descriptor delegate. + * + * @param element type + */ +class HelidonTestDescriptorDelegate implements HelidonTestDescriptor { + + private final HelidonTestDescriptor delegate; + + HelidonTestDescriptorDelegate(HelidonTestDescriptor delegate) { + this.delegate = delegate; + } + + @Override + public T element() { + return delegate.element(); + } + + @Override + public boolean resetPerTest() { + return delegate.resetPerTest(); + } + + @Override + public boolean pinningDetection() { + return delegate.pinningDetection(); + } + + @Override + public long pinningThreshold() { + return delegate.pinningThreshold(); + } + + @Override + public List addExtensions() { + return delegate.addExtensions(); + } + + @Override + public List addBeans() { + return delegate.addBeans(); + } + + @Override + public boolean addJaxRs() { + return delegate.addJaxRs(); + } + + @Override + public boolean disableDiscovery() { + return delegate.disableDiscovery(); + } + + @Override + public Optional configuration() { + return delegate.configuration(); + } + + @Override + public List addConfigs() { + return delegate.addConfigs(); + } + + @Override + public List addConfigBlocks() { + return delegate.addConfigBlocks(); + } + + @Override + public Stream annotations(Class aType, + Class cType, + Function function) { + return delegate.annotations(aType, cType, function); + } + + @Override + public Stream annotations(Class aType) { + return delegate.annotations(aType); + } +} diff --git a/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/HelidonTestDescriptorImpl.java b/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/HelidonTestDescriptorImpl.java new file mode 100644 index 00000000000..3046cbddeb1 --- /dev/null +++ b/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/HelidonTestDescriptorImpl.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2025 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.testing; + +import java.lang.reflect.AnnotatedElement; + +/** + * Default descriptor implementation. + */ +final class HelidonTestDescriptorImpl extends HelidonTestDescriptorBase { + + HelidonTestDescriptorImpl(T element) { + super(element); + } +} diff --git a/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/HelidonTestExtension.java b/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/HelidonTestExtension.java new file mode 100644 index 00000000000..ef8642947fa --- /dev/null +++ b/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/HelidonTestExtension.java @@ -0,0 +1,452 @@ +/* + * Copyright (c) 2025 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.testing; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.net.URI; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import io.helidon.common.LazyValue; + +import jakarta.annotation.Priority; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.context.Dependent; +import jakarta.enterprise.context.Destroyed; +import jakarta.enterprise.context.RequestScoped; +import jakarta.enterprise.event.Observes; +import jakarta.enterprise.inject.spi.AfterBeanDiscovery; +import jakarta.enterprise.inject.spi.Annotated; +import jakarta.enterprise.inject.spi.AnnotatedConstructor; +import jakarta.enterprise.inject.spi.AnnotatedField; +import jakarta.enterprise.inject.spi.AnnotatedMethod; +import jakarta.enterprise.inject.spi.AnnotatedParameter; +import jakarta.enterprise.inject.spi.AnnotatedType; +import jakarta.enterprise.inject.spi.BeanManager; +import jakarta.enterprise.inject.spi.BeforeBeanDiscovery; +import jakarta.enterprise.inject.spi.Extension; +import jakarta.enterprise.inject.spi.ProcessAnnotatedType; +import jakarta.enterprise.inject.spi.WithAnnotations; +import jakarta.enterprise.inject.spi.configurator.AnnotatedTypeConfigurator; +import jakarta.enterprise.util.AnnotationLiteral; +import jakarta.inject.Singleton; +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.client.WebTarget; + +import static io.helidon.microprofile.testing.ReflectionHelper.annotationHierarchy; +import static io.helidon.microprofile.testing.ReflectionHelper.invoke; +import static io.helidon.microprofile.testing.ReflectionHelper.isOverride; +import static io.helidon.microprofile.testing.ReflectionHelper.requireStatic; +import static io.helidon.microprofile.testing.ReflectionHelper.typeHierarchy; +import static jakarta.interceptor.Interceptor.Priority.PLATFORM_AFTER; +import static jakarta.interceptor.Interceptor.Priority.PLATFORM_BEFORE; + +/** + * Helidon test CDI extension. + */ +public abstract class HelidonTestExtension implements Extension { + + private static final Map, Annotation> ANNOTATION_LITERALS = Map.of( + ApplicationScoped.class, ApplicationScoped.Literal.INSTANCE, + Singleton.class, ApplicationScoped.Literal.INSTANCE, + RequestScoped.class, RequestScoped.Literal.INSTANCE, + Dependent.class, Dependent.Literal.INSTANCE); + + private static final Set> TYPE_ANNOTATION_TYPES = Set.of( + AddConfig.class, + AddConfigs.class, + AddConfigBlock.class, + Configuration.class); + + private static final Set> PARAMETER_ANNOTATION_TYPES = Set.of( + Socket.class); + + private static final Set> FIELD_ANNOTATION_TYPES = Set.of( + Socket.class); + + private static final Set> METHOD_ANNOTATION_TYPES = Set.of( + AddConfig.class, + AddConfigs.class, + AddConfigBlock.class, + AfterStop.class, + Configuration.class); + + private final HelidonTestInfo testInfo; + private final HelidonTestConfig testConfig; + private final HelidonTestScope testScope; + private final Map sockets = new HashMap<>(); + private final List afterStop = new ArrayList<>(); + + /** + * Create a new instance. + * + * @param testInfo test info + * @param testScope test scope + */ + protected HelidonTestExtension(HelidonTestInfo testInfo, HelidonTestScope testScope) { + this.testInfo = testInfo; + this.testConfig = new HelidonTestConfig(testInfo); + this.testScope = testScope; + } + + /** + * Get the annotation types usable on type. + * + * @return annotations + */ + protected Set> typeAnnotationTypes() { + return TYPE_ANNOTATION_TYPES; + } + + /** + * Get the annotation types usable on parameters. + * + * @return annotation types + */ + protected Set> parameterAnnotationTypes() { + return PARAMETER_ANNOTATION_TYPES; + } + + /** + * Get the annotation types usable on fields. + * + * @return annotation types + */ + protected Set> fieldAnnotationTypes() { + return FIELD_ANNOTATION_TYPES; + } + + /** + * Get the annotation types usable on methods. + * + * @return annotation types + */ + protected Set> methodAnnotationTypes() { + return METHOD_ANNOTATION_TYPES; + } + + /** + * Process a type annotation. + * + * @param annotation annotation + */ + protected void processTypeAnnotation(Annotation annotation) { + switch (annotation) { + case Configuration e -> processConfiguration(e); + case AddConfig e -> processAddConfig(e); + case AddConfigs e -> processAddConfig(e.value()); + case AddConfigBlock e -> processAddConfigBlock(e); + case AddConfigBlocks e -> processAddConfigBlock(e.value()); + default -> { + // no-op + } + } + } + + /** + * Process a parameter annotation. + * + * @param annotation annotation + */ + protected void processParameterAnnotation(Annotation annotation) { + if (annotation instanceof Socket s) { + processSocket(s, s.value()); + } + } + + /** + * Process a field annotation. + * + * @param annotation annotation + */ + protected void processFieldAnnotation(Annotation annotation) { + if (annotation instanceof Socket s) { + processSocket(s, s.value()); + } + } + + /** + * Process a static method annotation. + * + * @param annotation annotation + * @param method method + */ + protected void processStaticMethodAnnotation(Annotation annotation, Method method) { + if (annotation instanceof AfterStop) { + processAfterStop(method); + } else { + throw new IllegalStateException(String.format( + "@%s requires method %s to be non static", + method, annotation.annotationType().getSimpleName())); + } + } + + /** + * Process a test method annotation. + * + * @param annotation annotation + * @param method method + */ + protected void processTestMethodAnnotation(Annotation annotation, Method method) { + switch (annotation) { + case Configuration e -> processConfiguration(e); + case AddConfig e -> processAddConfig(e); + case AddConfigs e -> processAddConfig(e.value()); + case AddConfigBlock e -> processAddConfigBlock(e); + case AddConfigBlocks e -> processAddConfigBlock(e.value()); + default -> throw new IllegalStateException(String.format( + "@%s requires method %s to be static", + method, annotation.annotationType().getSimpleName())); + } + } + + /** + * Process a {@link Configuration} annotation. + * + * @param annotation annotation + */ + protected final void processConfiguration(Configuration annotation) { + testConfig.synthetic().update(annotation); + } + + /** + * Process {@link AddConfig Configuration} annotations. + * + * @param annotations annotations + */ + protected final void processAddConfig(AddConfig... annotations) { + testConfig.synthetic().update(annotations); + } + + /** + * Process {@link AddConfig Configuration} annotations. + * + * @param annotations annotations + */ + protected final void processAddConfigBlock(AddConfigBlock... annotations) { + testConfig.synthetic().update(annotations); + } + + /** + * Process a {@link AfterStop} method. + * + * @param method method + */ + protected final void processAfterStop(Method method) { + afterStop.add(requireStatic(method)); + } + + /** + * Process a {@link Socket} annotation. + * + * @param annotation annotation + * @param value value + */ + protected final void processSocket(Annotation annotation, String value) { + sockets.put(annotation, value); + } + + private void processTestClass(@Observes + @WithAnnotations(HelidonTestScoped.class) + ProcessAnnotatedType pat, + BeanManager bm) { + + Set> allTypes = new HashSet<>(); + allTypes.add(pat.getAnnotatedType()); + + // create annotated types for the type hierarchy + for (Class type : typeHierarchy(pat.getAnnotatedType().getJavaClass(), false)) { + allTypes.add(bm.createAnnotatedType(type)); + } + + Method testMethod = testInfo.testMethod().orElse(null); + for (AnnotatedType type : allTypes) { + + // type + processAnnotated(type, typeAnnotationTypes(), this::processTypeAnnotation); + + // constructor parameters + for (AnnotatedConstructor constructor : type.getConstructors()) { + for (AnnotatedParameter parameter : constructor.getParameters()) { + processAnnotated(parameter, fieldAnnotationTypes(), this::processParameterAnnotation); + } + } + + // fields + for (AnnotatedField field : type.getFields()) { + processAnnotated(field, parameterAnnotationTypes(), this::processFieldAnnotation); + } + + // methods + for (AnnotatedMethod method : type.getMethods()) { + processAnnotated(method, methodAnnotationTypes(), a -> { + if (method.isStatic()) { + processStaticMethodAnnotation(a, method.getJavaMember()); + } else { + // test method or super test method + if (testMethod != null && isOverride(method.getJavaMember(), testMethod)) { + processTestMethodAnnotation(a, method.getJavaMember()); + } + } + }); + } + } + } + + private void beforeBeanDiscovery(@Observes BeforeBeanDiscovery event, BeanManager bm) { + // remove bootstrap config + testConfig.resolve(); + + // add the test class + event.addAnnotatedType(testInfo.testClass(), "HelidonTest") + .add(HelidonTestScoped.Literal.INSTANCE); + + for (AddBean addBean : testInfo.addBeans()) { + Class beanClass = addBean.value(); + Class scopeClass = addBean.scope(); + AnnotatedTypeConfigurator configurator = event.addAnnotatedType(beanClass, beanClass.getName()); + + // process scope + if (!scopeClass.equals(Annotation.class)) { + // scope explicitly configured + Annotation scope = ANNOTATION_LITERALS.get(scopeClass); + if (scope == null) { + scope = new AnnotationLiteral<>() { + @Override + public Class annotationType() { + return scopeClass; + } + }; + } + // remove existing scope annotations + configurator.remove(a -> bm.isScope(a.annotationType())); + configurator.add(scope); + } else { + // no scope configured + AnnotatedType annotated = configurator.getAnnotated(); + if (annotated.getAnnotations().stream() + .noneMatch(a -> bm.isScope(a.annotationType()))) { + + // bean class does not have a scope annotation + // default to ApplicationScoped + configurator.add(ApplicationScoped.Literal.INSTANCE); + } + } + } + } + + private void afterBeanDiscovery(@Observes + @Priority(PLATFORM_BEFORE) + AfterBeanDiscovery event, + BeanManager bm) { + + // useExisting may have changed, re-resolve + testConfig.resolve(); + + // test scope support + event.addContext(testScope); + + Class serverClass = serverClass(); + if (serverClass != null && (!testInfo.disableDiscovery() || testInfo.containsExtension(serverClass))) { + + Extension server = bm.getExtension(serverClass); + Client client = ClientBuilder.newClient(); + + // default port + event.addBean() + .addTransitiveTypeClosure(WebTarget.class) + .scope(ApplicationScoped.class) + .createWith(c -> client.target("http://localhost:" + port(server, "@default"))); + + // named ports + sockets.forEach((annotation, value) -> { + + Supplier supplier = () -> "http://localhost:" + port(server, value); + + event.addBean() + .addTransitiveTypeClosure(WebTarget.class) + .scope(ApplicationScoped.class) + .qualifiers(annotation) + .createWith(c -> client.target(supplier.get())); + + // unproxiable types, use dependent backed by lazy values + + LazyValue uri = LazyValue.create(() -> URI.create(supplier.get())); + event.addBean() + .addType(URI.class) + .scope(Dependent.class) + .qualifiers(annotation) + .createWith(c -> uri.get()); + + LazyValue rawUri = LazyValue.create(supplier); + event.addBean() + .addType(String.class) + .scope(Dependent.class) + .qualifiers(annotation) + .createWith(c -> rawUri.get()); + }); + } + } + + private void afterStop(@Observes + @Priority(PLATFORM_AFTER) + @Destroyed(ApplicationScoped.class) + Object ignored) { + + afterStop.forEach(m -> invoke(Void.class, m, null)); + } + + private void processAnnotated(Annotated elt, Set> types, Consumer action) { + for (Annotation a : elt.getAnnotations()) { + if (types.contains(a.annotationType())) { + action.accept(a); + } else { + // meta-annotations + for (Annotation b : annotationHierarchy(a.annotationType())) { + if (types.contains(b.annotationType())) { + action.accept(b); + } + } + } + } + } + + @SuppressWarnings("unchecked") + private static Class serverClass() { + try { + return (Class) Class.forName("io.helidon.microprofile.server.ServerCdiExtension"); + } catch (ClassNotFoundException ignored) { + return null; + } + } + + private static int port(Extension server, String name) { + try { + Method portMethod = server.getClass().getMethod("port", String.class); + return invoke(Integer.class, portMethod, server, name); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + } +} diff --git a/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/HelidonTestInfo.java b/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/HelidonTestInfo.java new file mode 100644 index 00000000000..c51146ce331 --- /dev/null +++ b/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/HelidonTestInfo.java @@ -0,0 +1,362 @@ +/* + * Copyright (c) 2025 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.testing; + +import java.lang.annotation.Annotation; +import java.lang.ref.SoftReference; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; +import java.util.stream.Stream; + +import static io.helidon.microprofile.testing.Instrumented.isInstrumented; +import static java.util.stream.Collectors.joining; + +/** + * Metadata of the test class or method that triggers the creation of a CDI container. + * + * @param element type + */ +public sealed interface HelidonTestInfo extends HelidonTestDescriptor + permits HelidonTestInfo.ClassInfo, + HelidonTestInfo.MethodInfo { + + /** + * Create a new class info. + * + * @param cls class + * @return ClassInfo + */ + static ClassInfo classInfo(Class cls) { + return classInfo(cls, HelidonTestDescriptorImpl::new); + } + + /** + * Create a new class info. + * + * @param cls class + * @param function descriptor factory + * @return ClassInfo + */ + static ClassInfo classInfo(Class cls, Function, HelidonTestDescriptor>> function) { + Class theClass = isInstrumented(cls) ? cls.getSuperclass() : cls; + return ClassInfo.CACHE.compute(theClass.getName(), (e, r) -> { + if (r == null || r.get() == null) { + return new SoftReference<>(new ClassInfo(function.apply(cls))); + } + return r; + }).get(); + } + + /** + * Create a new class info. + * + * @param desc class descriptor + * @return ClassInfo + */ + static ClassInfo classInfo(HelidonTestDescriptor> desc) { + return ClassInfo.CACHE.compute(desc.element().getName(), (e, r) -> { + if (r == null || r.get() == null) { + return new SoftReference<>(new ClassInfo(desc)); + } + return r; + }).get(); + } + + /** + * Create a new method info. + * + * @param element method + * @param classInfo class info + * @return MethodInfo + */ + static MethodInfo methodInfo(Method element, ClassInfo classInfo) { + return methodInfo(element, classInfo, HelidonTestDescriptorImpl::new); + } + + /** + * Create a new method info. + * + * @param method method + * @param classInfo class info + * @param function descriptor factory + * @return MethodInfo + */ + static MethodInfo methodInfo(Method method, ClassInfo classInfo, Function> function) { + return MethodInfo.CACHE.compute(MethodInfo.cacheKey(method, classInfo), (e, r) -> { + if (r == null || r.get() == null) { + return new SoftReference<>(new MethodInfo(function.apply(method), classInfo)); + } + return r; + }).get(); + } + + /** + * Create a new method info. + * + * @param desc method descriptor + * @param classInfo class info + * @return MethodInfo + */ + static MethodInfo methodInfo(HelidonTestDescriptor desc, ClassInfo classInfo) { + return MethodInfo.CACHE.compute(MethodInfo.cacheKey(desc.element(), classInfo), (e, r) -> { + if (r == null || r.get() == null) { + return new SoftReference<>(new MethodInfo(desc, classInfo)); + } + return r; + }).get(); + } + + /** + * Get the id. + * + * @return id + */ + String id(); + + /** + * Get the test class. + * + * @return test class + */ + Class testClass(); + + /** + * Get the test method. + * + * @return test method + */ + default Optional testMethod() { + return Optional.empty(); + } + + /** + * Get the class info. + * + * @return ClassInfo + */ + ClassInfo classInfo(); + + /** + * Indicate if the container should be reset. + * For a class this is resolved via {@code HelidonTest#resetPerTest()}. + * For a method this is inferred if any of the following annotations is used: + *

+ * + * @return {@code true} if reset is required, {@code false} otherwise + */ + default boolean requiresReset() { + return false; + } + + /** + * Class info. + */ + final class ClassInfo extends HelidonTestDescriptorDelegate> implements HelidonTestInfo> { + private static final Map> CACHE = new ConcurrentHashMap<>(); + + private ClassInfo(HelidonTestDescriptor> descriptor) { + super(descriptor); + } + + @Override + public String id() { + return element().getName(); + } + + @Override + public Class testClass() { + return element(); + } + + @Override + public ClassInfo classInfo() { + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof ClassInfo that)) { + return false; + } + return Objects.equals(element().getName(), that.element().getName()); + } + + @Override + public int hashCode() { + return element().getName().hashCode(); + } + + @Override + public String toString() { + return new PrettyPrinter() + .object(PrettyPrinters.classInfo(this)) + .toString(); + } + } + + /** + * Method info. + */ + final class MethodInfo implements HelidonTestInfo { + private static final Map> CACHE = new ConcurrentHashMap<>(); + + private final HelidonTestDescriptor descriptor; + private final ClassInfo classInfo; + + private MethodInfo(HelidonTestDescriptor descriptor, ClassInfo classInfo) { + this.descriptor = descriptor; + this.classInfo = classInfo; + } + + private static String cacheKey(Method method, ClassInfo classInfo) { + return classInfo.element().getName() + "#" + + method.getName() + + Arrays.stream(method.getParameterTypes()) + .map(Type::getTypeName) + .collect(joining(",", "(", ")")); + } + + @Override + public String id() { + return classInfo.id() + "#" + element().getName(); + } + + @Override + public Class testClass() { + return classInfo.element(); + } + + @Override + public Optional testMethod() { + return Optional.of(descriptor.element()); + } + + @Override + public ClassInfo classInfo() { + return classInfo; + } + + @Override + public Method element() { + return descriptor.element(); + } + + @Override + public List addExtensions() { + return concat(classInfo.addExtensions(), descriptor.addExtensions()); + } + + @Override + public List addBeans() { + return concat(classInfo.addBeans(), descriptor.addBeans()); + } + + @Override + public boolean addJaxRs() { + return classInfo.addJaxRs() || descriptor.addJaxRs(); + } + + @Override + public boolean disableDiscovery() { + return classInfo.disableDiscovery() || descriptor.disableDiscovery(); + } + + @Override + public Optional configuration() { + return descriptor.configuration(); + } + + @Override + public List addConfigs() { + return concat(classInfo.addConfigs(), descriptor.addConfigs()); + } + + @Override + public List addConfigBlocks() { + return concat(classInfo.addConfigBlocks(), descriptor.addConfigBlocks()); + } + + @Override + public Stream annotations(Class aType, + Class cType, + Function function) { + return Stream.concat( + descriptor.annotations(aType, cType, function), + classInfo.annotations(aType, cType, function)); + } + + @Override + public Stream annotations(Class aType) { + return Stream.concat( + descriptor.annotations(aType), + classInfo.annotations(aType)); + } + + @Override + public boolean requiresReset() { + return classInfo.resetPerTest() + || descriptor.configuration().isPresent() + || descriptor.disableDiscovery() + || descriptor.addJaxRs() + || !descriptor.addBeans().isEmpty() + || !descriptor.addExtensions().isEmpty(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof MethodInfo that)) { + return false; + } + return Objects.equals(element().getName(), that.element().getName()); + } + + @Override + public int hashCode() { + return element().hashCode(); + } + + @Override + public String toString() { + return new PrettyPrinter() + .object(PrettyPrinters.methodInfo(this)) + .toString(); + } + + private static List concat(List l1, List l2) { + return Stream.concat(l1.stream(), l2.stream()).toList(); + } + } +} diff --git a/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/HelidonTestScope.java b/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/HelidonTestScope.java new file mode 100644 index 00000000000..2310da28cc9 --- /dev/null +++ b/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/HelidonTestScope.java @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2025 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.testing; + +import java.lang.annotation.Annotation; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +import jakarta.enterprise.context.spi.Context; +import jakarta.enterprise.context.spi.Contextual; +import jakarta.enterprise.context.spi.CreationalContext; +import jakarta.enterprise.inject.spi.Bean; + +/** + * CDI context that supports {@link HelidonTestScoped}. + */ +public abstract sealed class HelidonTestScope implements Context permits HelidonTestScope.PerThread, + HelidonTestScope.PerContainer { + + /** + * Create a new per-thread scope. + * + * @return HelidonTestScope + */ + public static HelidonTestScope ofThread() { + return new PerThread(); + } + + /** + * Create a new per-container scope. + * + * @return HelidonTestScope + */ + public static HelidonTestScope ofContainer() { + return new PerContainer(); + } + + @Override + public Class getScope() { + return HelidonTestScoped.class; + } + + @Override + public boolean isActive() { + return true; + } + + @Override + public T get(Contextual contextual, CreationalContext context) { + return instances().get(contextual, context); + } + + @Override + public T get(Contextual contextual) { + return instances().get(contextual).orElse(null); + } + + /** + * Close the scope. + */ + public void close() { + instances().destroy(); + } + + /** + * Get the instances. + * + * @return instances + */ + abstract Instances instances(); + + /** + * Instances per thread. + */ + static final class PerThread extends HelidonTestScope { + private static final ThreadLocal THREAD_LOCAL = ThreadLocal.withInitial(Instances::new); + + @Override + Instances instances() { + return THREAD_LOCAL.get(); + } + + @Override + public void close() { + THREAD_LOCAL.remove(); + super.close(); + } + } + + /** + * Instances per container. + */ + static final class PerContainer extends HelidonTestScope { + private final Instances instances = new Instances(new ConcurrentHashMap<>()); + + @Override + Instances instances() { + return instances; + } + } + + private record Instances(Map, Instance> map) { + + Instances() { + this(new HashMap<>()); + } + + @SuppressWarnings("unchecked") + Instance create(Contextual contextual, CreationalContext context) { + return (Instance) map.computeIfAbsent(contextual, k -> new Instance<>((Bean) k, context)); + } + + T get(Contextual contextual, CreationalContext context) { + return get(contextual).orElseGet(() -> create(contextual, context).it); + } + + @SuppressWarnings("unchecked") + Optional get(Contextual contextual) { + return Optional.ofNullable((Instance) map.get(contextual)).map(Instance::it); + } + + void destroy() { + map.values().forEach(Instance::destroy); + } + } + + private record Instance(Bean bean, CreationalContext context, T it) { + Instance(Bean bean, CreationalContext context) { + this(bean, context, bean.create(context)); + } + + void destroy() { + bean.destroy(it, context); + } + } +} diff --git a/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/HelidonTestScoped.java b/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/HelidonTestScoped.java new file mode 100644 index 00000000000..01843a3fc95 --- /dev/null +++ b/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/HelidonTestScoped.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2025 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.testing; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import jakarta.enterprise.context.NormalScope; +import jakarta.enterprise.util.AnnotationLiteral; + +/** + * CDI scope used for the test class. + */ +@NormalScope +@Retention(RetentionPolicy.RUNTIME) +@interface HelidonTestScoped { + + /** + * Annotation literal. + */ + @SuppressWarnings("ALL") + final class Literal extends AnnotationLiteral implements HelidonTestScoped { + + static final Literal INSTANCE = new Literal(); + } +} diff --git a/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/Instrumented.java b/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/Instrumented.java new file mode 100644 index 00000000000..e83c8c19d3f --- /dev/null +++ b/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/Instrumented.java @@ -0,0 +1,158 @@ +/* + * Copyright (c) 2025 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.testing; + +import java.lang.annotation.Annotation; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.List; +import java.util.function.BiFunction; +import java.util.stream.Stream; + +import net.bytebuddy.ByteBuddy; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.dynamic.DynamicType; +import net.bytebuddy.dynamic.loading.ClassLoadingStrategy; +import net.bytebuddy.dynamic.scaffold.subclass.ConstructorStrategy; +import net.bytebuddy.implementation.InvocationHandlerAdapter; +import net.bytebuddy.implementation.attribute.MethodAttributeAppender; +import net.bytebuddy.matcher.ElementMatcher; + +import static io.helidon.microprofile.testing.ReflectionHelper.invoke; +import static net.bytebuddy.matcher.ElementMatchers.isAnnotatedWith; +import static net.bytebuddy.matcher.ElementMatchers.isEquals; +import static net.bytebuddy.matcher.ElementMatchers.isHashCode; +import static net.bytebuddy.matcher.ElementMatchers.isToString; +import static net.bytebuddy.matcher.ElementMatchers.not; + +/** + * Marker interface for instrumented type. + */ +public interface Instrumented { + + /** + * Instantiate a class without running constructors. + * + * @param type type + * @param type + * @return instance + */ + static T allocateInstance(Class type) { + try { + Field field = sun.misc.Unsafe.class.getDeclaredField("theUnsafe"); + field.setAccessible(true); + sun.misc.Unsafe unsafe = (sun.misc.Unsafe) field.get(null); + // allocateInstance is OK to use (not planned for removal by JEP471) + return type.cast(unsafe.allocateInstance(type)); + } catch (InstantiationException + | IllegalAccessException + | NoSuchFieldException e) { + throw new RuntimeException(e); + } + } + + /** + * Test if the given type is instrumented. + * + * @param type type + * @return {@code true} if instrumented, {@code false} otherwise + */ + static boolean isInstrumented(Class type) { + for (Class iface : type.getInterfaces()) { + if (iface.equals(Instrumented.class)) { + return true; + } + } + return false; + } + + /** + * Create an instrumented class. + * + * @param type type + * @param annotations annotations to add to the proxy class + * @param methodExcludes method annotations that skip interception + * @param resolver function to resolve the delegate + * @param type + * @return instrumented class + */ + static Class instrument(Class type, + List annotations, + List> methodExcludes, + BiFunction, Method, T> resolver) { + + return instrument(type, annotations, methodExcludes, (proxy, method, args) -> { + T instance = resolver.apply(type, method); + if (instance != null) { + return invoke(Object.class, method, instance, args); + } + return null; + }); + } + + /** + * Create an instrumented class. + * + * @param type type + * @param annotations annotations to add to the proxy class + * @param methodExcludes method annotations that skip interception + * @param handler invocation handler + * @param type + * @return instrumented class + */ + static Class instrument(Class type, + List annotations, + List> methodExcludes, + InvocationHandler handler) { + + // always skip delegation for equals, hashCode, toString + // the goal is to instantiate the delegate for "concrete" methods + ElementMatcher.Junction matcher = not(isEquals()) + .and(not(isHashCode())) + .and(not(isToString())); + + // also skip delegation for methods annotated with the given annotations + for (Class exclude : methodExcludes) { + matcher = matcher.and(not(isAnnotatedWith(exclude))); + } + + try (DynamicType.Unloaded unloaded = new ByteBuddy() + // preserve constructors annotations + // must-have for constructor injection + .subclass(type, ConstructorStrategy.Default.IMITATE_SUPER_CLASS.withInheritedAnnotations()) + .implement(Instrumented.class) + // repeat type annotations and add the given annotations + .annotateType(Stream.concat( + Arrays.stream(type.getDeclaredAnnotations()), + annotations.stream()) + .toArray(Annotation[]::new)) + .withHashCodeEquals() + .method(matcher) + .intercept(InvocationHandlerAdapter.of(handler)) + // repeat all method annotations on the subclass + .attribute(MethodAttributeAppender.ForInstrumentedMethod.INCLUDING_RECEIVER) + .make()) { + MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(type, MethodHandles.lookup()); + return unloaded.load(type.getClassLoader(), ClassLoadingStrategy.UsingLookup.of(lookup)) + .getLoaded(); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } +} diff --git a/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/PrettyPrinter.java b/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/PrettyPrinter.java new file mode 100644 index 00000000000..98dab5155a5 --- /dev/null +++ b/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/PrettyPrinter.java @@ -0,0 +1,194 @@ +/* + * Copyright (c) 2025 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.testing; + +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Function; + +/** + * Pretty printer. + */ +@SuppressWarnings({"UnusedReturnValue", "SameParameterValue"}) +final class PrettyPrinter { + + private final StringBuilder sb = new StringBuilder(); + private int i = 0; + + /** + * Empty consumer. + */ + static final Consumer EMPTY = printer -> { + }; + + /** + * Print a name. + * + * @param name name + * @return this instance + */ + PrettyPrinter name(String name) { + if (name != null && !name.isEmpty()) { + value(name).append(" = "); + } + return this; + } + + /** + * Append a value. + * + * @param value value + * @return this instance + */ + PrettyPrinter value(Object value) { + return append('\n').indent().append(value); + } + + /** + * Append a named value. + * + * @param key key + * @param value value + * @return this instance + */ + PrettyPrinter value(String key, Object value) { + return name(key).append(value); + } + + /** + * Append a named block. + * + * @param key key + * @param block block + * @return this instance + */ + PrettyPrinter block(String key, String block) { + name(key).append("<<"); + block.lines().forEach(this::value); + value(">>"); + return this; + } + + /** + * Append an object structure. + * + * @param action action + * @return this instance + */ + PrettyPrinter object(Consumer action) { + append('{') + .indent(() -> action.accept(this)) + .append('\n').indent().append('}'); + return this; + } + + /** + * Append a named object structure. + * No-op if {@code action} is {@link #EMPTY}. + * + * @param action action + * @return this instance + */ + PrettyPrinter object(String name, Consumer action) { + if (action != EMPTY) { + name(name).object(action); + } + return this; + } + + /** + * Repeat a named object structure for the given list. + * + * @param name name, may be {@code null} + * @param list list + * @param function function + * @param list element type + * @return this instance + */ + PrettyPrinter objects(String name, List list, Function> function) { + if (list != null) { + list.forEach(e -> object(name, function.apply(e))); + } + return this; + } + + /** + * Append a named list structure. + * + * @param name name, may be {@code null} + * @param list list + * @param mapper mapper + * @param list element type + * @return this instance + */ + PrettyPrinter values(String name, List list, Function mapper) { + if (list != null && !list.isEmpty()) { + name(name).append('[') + .indent(() -> list.forEach(e -> value(mapper.apply(e)))) + .append('\n').indent().append(']'); + } + return this; + } + + /** + * Append a named list structure. + * + * @param name name, may be {@code null} + * @param values values + * @return this instance + */ + @SafeVarargs + final PrettyPrinter values(String name, T... values) { + return values(name, Arrays.asList(values), e -> e); + } + + /** + * Delegate to the given action. + * + * @param action action + * @return this instance + */ + PrettyPrinter apply(Consumer action) { + action.accept(this); + return this; + } + + @Override + public String toString() { + return sb.toString(); + } + + private PrettyPrinter append(Object object) { + sb.append(object); + return this; + } + + private PrettyPrinter indent() { + sb.append(" ".repeat(i)); + return this; + } + + private PrettyPrinter indent(Runnable action) { + try { + i++; + action.run(); + return this; + } finally { + i--; + } + } +} diff --git a/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/PrettyPrinters.java b/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/PrettyPrinters.java new file mode 100644 index 00000000000..0f30b6e0aa9 --- /dev/null +++ b/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/PrettyPrinters.java @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2025 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.testing; + +import java.util.function.Consumer; + +import io.helidon.microprofile.testing.HelidonTestInfo.ClassInfo; +import io.helidon.microprofile.testing.HelidonTestInfo.MethodInfo; + +/** + * Pretty printers. + */ +class PrettyPrinters { + + private PrettyPrinters() { + } + + /** + * Create a {@link HelidonTestInfo} printer. + * + * @param info info + * @return printer consumer + */ + static Consumer testInfo(HelidonTestInfo info) { + return switch (info) { + case ClassInfo classInfo -> classInfo(classInfo); + case MethodInfo methodInfo -> methodInfo(methodInfo); + }; + } + + /** + * Create a {@link MethodInfo} printer. + * + * @param info info + * @return printer consumer + */ + static Consumer methodInfo(MethodInfo info) { + return printer -> printer + .value("class", info.classInfo().element().getName()) + .value("method", info.element().getName()) + .value("requiresReset", info.requiresReset()) + .apply(testDescriptor(info)); + } + + /** + * Create a {@link ClassInfo} printer. + * + * @param info info + * @return printer consumer + */ + static Consumer classInfo(ClassInfo info) { + return printer -> printer + .value("class", info.element().getName()) + .apply(testDescriptor(info)); + } + + /** + * Create a {@link HelidonTestDescriptor} printer. + * + * @param desc descriptor + * @return printer consumer + */ + static Consumer testDescriptor(HelidonTestDescriptor desc) { + return printer -> printer + .value("resetPerTest", desc.resetPerTest()) + .value("pinningDetection", desc.pinningDetection()) + .value("pinningThreshold", desc.pinningThreshold()) + .value("disableDiscovery", desc.disableDiscovery()) + .values("addExtensions", desc.addExtensions(), a -> a.value().getName()) + .values("addBeans", desc.addBeans(), a -> a.value().getName()) + .object("configuration", desc.configuration() + .map(PrettyPrinters::configuration) + .orElse(PrettyPrinter.EMPTY)) + .objects("addConfig", desc.addConfigs(), PrettyPrinters::addConfig) + .objects("addConfigBlock", desc.addConfigBlocks(), PrettyPrinters::addConfigBlock); + } + + /** + * Create a {@link Configuration} printer. + * + * @param a annotation + * @return printer consumer + */ + static Consumer configuration(Configuration a) { + return printer -> printer + .object("configuration", p -> p + .value("useExisting", a.useExisting()) + .value("profile", a.profile()) + .values("configSources", a.configSources())); + } + + /** + * Create a {@link AddConfig} printer. + * + * @param a annotation + * @return printer consumer + */ + static Consumer addConfig(AddConfig a) { + return printer -> printer + .value("key", a.key()) + .value("value", a.value()); + } + + /** + * Create a {@link AddConfigBlock} printer. + * + * @param a annotation + * @return printer consumer + */ + static Consumer addConfigBlock(AddConfigBlock a) { + return printer -> printer + .value("type", a.type()) + .block("value", a.value()); + } +} diff --git a/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/Proxies.java b/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/Proxies.java new file mode 100644 index 00000000000..a9247e924fa --- /dev/null +++ b/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/Proxies.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2025 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.testing; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.List; +import java.util.function.Function; + +import static io.helidon.microprofile.testing.ReflectionHelper.invoke; + +/** + * Proxy helper. + */ +public class Proxies { + + private Proxies() { + // cannot be instantiated + } + + /** + * Mirror an annotation. + * + * @param type target type + * @param annotation source annotation + * @param target type + * @return mirror + */ + public static T mirror(Class type, Annotation annotation) { + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + Object o = Proxy.newProxyInstance(cl, new Class[] {type}, (proxy, method, args) -> { + Method sourceMethod = annotation.getClass().getMethod(method.getName()); + return invoke(Object.class, sourceMethod, annotation); + }); + return type.cast(o); + } + + /** + * Instantiate an annotation. + * + * @param type annotation type + * @param function attributes function + * @param annotation type + * @return annotation + */ + public static T annotation(Class type, Function function) { + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + Object o = Proxy.newProxyInstance(cl, List.of(type).toArray(Class[]::new), + (proxy, method, args) -> { + String methodName = method.getName(); + if ("annotationType".equals(methodName)) { + return type; + } + Object value = function.apply(methodName); + return value != null ? value : method.getDefaultValue(); + }); + return type.cast(o); + } +} diff --git a/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/ReflectionHelper.java b/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/ReflectionHelper.java new file mode 100644 index 00000000000..e9c1aed8492 --- /dev/null +++ b/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/ReflectionHelper.java @@ -0,0 +1,298 @@ +/* + * Copyright (c) 2025 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.testing; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Deque; +import java.util.List; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Reflection helper. + */ +class ReflectionHelper { + + private ReflectionHelper() { + // cannot be instanciated + } + + /** + * Collect all the methods that have the given method signature in its type hierarchy. + * + * @param method method + * @return hierarchy + */ + static List methodHierarchy(Method method) { + return typeHierarchy(method.getDeclaringClass(), true).stream() + .flatMap(t -> Stream.of(t.getDeclaredMethods())) + .filter(override -> isOverride(method, override)) + .toList(); + } + + /** + * Test if the given method overrides another. + * + * @param method base method + * @param override override method + * @return {@code true} if overrides, {@code false otherwise} + */ + static boolean isOverride(Method method, Method override) { + return override.getName().equals(method.getName()) + && override.getReturnType().isAssignableFrom(method.getReturnType()) + && Arrays.equals(override.getParameterTypes(), method.getParameterTypes()); + } + + /** + * Collect all types in the type hiearchy of the given type. + * + * @param type type + * @param all {@code false} to only include ancestors + * @return types + */ + static List> typeHierarchy(Class type, boolean all) { + List> result = new ArrayList<>(); + Deque> stack = new ArrayDeque<>(); + stack.push(type); + while (!stack.isEmpty()) { + Class e = stack.pop(); + if (e.getPackage().getName().startsWith("java.")) { + continue; + } + if (all || !e.equals(type)) { + result.add(e); + } + if (e.getSuperclass() != null) { + stack.push(e.getSuperclass()); + } + List.of(e.getInterfaces()).forEach(stack::push); + } + return result; + } + + /** + * Collect all effective annotations of an annotation class. + * + * @param annotationType annotation type + * @return annotations + */ + static List annotationHierarchy(Class annotationType) { + List result = new ArrayList<>(); + Deque stack = new ArrayDeque<>(); + List.of(annotationType.getAnnotations()).forEach(stack::push); + while (!stack.isEmpty()) { + Annotation e = stack.pop(); + Class type = e.annotationType(); + if (!type.getPackage().getName().startsWith("io.helidon.")) { + continue; + } + result.add(e); + List.of(type.getAnnotations()).forEach(stack::push); + } + return result; + } + + /** + * Annotated element. + * + * @param element element + * @param annotations annotations + */ + record Annotated(AnnotatedElement element, List annotations) { + + private static Annotated create(AnnotatedElement element) { + return new Annotated<>(element, Stream.of(element.getAnnotations()) + .flatMap(a -> Stream.concat(Stream.of(a), annotationHierarchy(a.annotationType()).stream())) + .toList()); + } + } + + /** + * Get all annotations for a method and its hierarchy. + * + * @param method method + * @return annotations + */ + static List> annotated(Method method) { + return annotated(methodHierarchy(method)); + } + + /** + * Get all annotations for a class and its hierarchy. + * + * @param type type + * @return annotations + */ + static List> annotated(Class type) { + return annotated(typeHierarchy(type, true)); + } + + /** + * Get all annotations for the given elements. + * + * @param elements elements + * @return annotations + */ + static List> annotated(List elements) { + return elements.stream().map(Annotated::create).collect(Collectors.toList()); + } + + /** + * Filter annotations of a given type. + * + * @param annotated annotations + * @param aType annotation type + * @param cType container type + * @param function function to inflate from container + * @param annotation type + * @param container type + * @return annotations + */ + static List> filterAnnotated(List> annotated, + Class aType, + Class cType, + Function function) { + + Predicate predicate = a -> a.annotationType().equals(cType) || a.annotationType().equals(aType); + return annotated.stream() + .filter(a -> a.annotations.stream().anyMatch(predicate)) + .map(it -> new Annotated<>(it.element, it.annotations.stream() + .filter(predicate) + .flatMap(at -> { + if (at.annotationType().equals(cType)) { + return Stream.of(function.apply(cType.cast(at))); + } + return Stream.of(aType.cast(at)); + }) + .toList())) + .toList(); + } + + /** + * Filter annotations of a given type. + * + * @param annotated annotations + * @param aType annotation type + * @param container type + * @return annotations + */ + static Stream> filterAnnotated(List> annotated, Class aType) { + Predicate predicate = a -> a.annotationType().equals(aType); + return annotated.stream() + .filter(a -> a.annotations.stream().anyMatch(predicate)) + .map(it -> new Annotated<>(it.element, it.annotations.stream() + .filter(predicate) + .map(aType::cast) + .toList())); + } + + /** + * Filter annotations of a given type. + * + * @param annotated annotations + * @param aType annotation type + * @param cType container type + * @param function function to inflate from container + * @param annotation type + * @param container type + * @return annotations + */ + static Stream filterAnnotations(List> annotated, + Class aType, + Class cType, + Function function) { + + return filterAnnotated(annotated, aType, cType, function).stream() + .flatMap(it -> it.annotations.stream()); + } + + /** + * Filter annotations of a given type. + * + * @param annotations annotations + * @param annotationType annotation type + * @param container type + * @return annotations + */ + static Stream filterAnnotations(List> annotations, Class annotationType) { + return filterAnnotated(annotations, annotationType) + .flatMap(it -> it.annotations.stream()); + } + + /** + * Checks that the given class is public. + * + * @param cls class + * @param class type + * @return Class + * @throws java.lang.IllegalArgumentException if the given class is not public + */ + static Class requirePublic(Class cls) throws IllegalArgumentException { + if (!Modifier.isPublic(cls.getModifiers())) { + throw new IllegalArgumentException(cls.getName() + " is not public"); + } + return cls; + } + + /** + * Checks that the given method is static. + * + * @param method method + * @return Method + * @throws java.lang.IllegalArgumentException if the given class is not public + */ + static Method requireStatic(Method method) throws IllegalArgumentException { + if (!Modifier.isStatic(method.getModifiers())) { + throw new IllegalArgumentException(method + " is not static"); + } + return method; + } + + /** + * Invoke a method. + * + * @param type return type + * @param method method + * @param instance instance + * @param args arguments + * @param return type + * @return invocation result + */ + static T invoke(Class type, Method method, Object instance, Object... args) { + try { + method.setAccessible(true); + Object value = method.invoke(instance, args); + return type.cast(value); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } catch (InvocationTargetException e) { + Throwable target = e.getTargetException(); + if (e.getTargetException() instanceof RuntimeException re) { + throw re; + } + throw new RuntimeException(target); + } + } +} diff --git a/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/Socket.java b/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/Socket.java new file mode 100644 index 00000000000..939354ce058 --- /dev/null +++ b/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/Socket.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2022, 2025 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.testing; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import jakarta.inject.Qualifier; + +/** + * CDI qualifier to inject a JAX-RS client or URI for a named socket. + *

+ * The supported types are: + *

    + *
  • {@link jakarta.ws.rs.client.WebTarget WebTarget} a JAXRS client target
  • + *
  • {@link java.net.URI URI} a URI
  • + *
  • {@link String} a raw URI
  • + *
+ *

+ * This annotation can be used on constructor parameters, or class fields. + * Test method parameter injection may be supported depending on the test framework integration. + *

+ * Also note that the default socket name is {@code "@default"}. + *

+ * E.g. constructor injection: + *

+ * class MyTest {
+ *     private final WebTarget target;
+ *
+ *     @Inject
+ *     MyTest(@Socket("@default") URI uri) {
+ *         target = ClientBuilder.newClient().target(uri);
+ *     }
+ * }
+ * 
+ *

+ * E.g. field injection: + *

+ * class MyTest {
+ *
+ *     @Inject // optional
+ *     @Socket("@default")
+ *     private WebTarget target;
+ * }
+ * 
+ */ +@Qualifier +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD, ElementType.PARAMETER}) +public @interface Socket { + + /** + * Name of the socket. + * + * @return socket name + */ + String value(); +} diff --git a/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/package-info.java b/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/package-info.java new file mode 100644 index 00000000000..4ad81a5fccf --- /dev/null +++ b/microprofile/testing/testing/src/main/java/io/helidon/microprofile/testing/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2025 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. + */ + +/** + * Helidon MicroProfile Testing Support. + */ +package io.helidon.microprofile.testing; diff --git a/microprofile/testing/testing/src/main/java/module-info.java b/microprofile/testing/testing/src/main/java/module-info.java new file mode 100644 index 00000000000..ae18ec48378 --- /dev/null +++ b/microprofile/testing/testing/src/main/java/module-info.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2025 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. + */ + +/** + * Helidon MicroProfile Testing Support. + */ +@SuppressWarnings("requires-transitive-automatic") +module io.helidon.microprofile.testing { + + requires transitive io.helidon.common.testing.vitualthreads; + requires io.helidon.config.mp; + requires io.helidon.config.yaml.mp; + requires io.helidon.microprofile.cdi; + requires jakarta.inject; + + requires transitive jakarta.cdi; + requires transitive jakarta.ws.rs; + + requires static transitive io.helidon.microprofile.server; + requires static transitive jersey.cdi1x; + requires static transitive jersey.weld2.se; + + requires jersey.client; + requires net.bytebuddy; + requires jdk.unsupported; + + exports io.helidon.microprofile.testing; +} diff --git a/microprofile/testing/testing/src/test/java/io/helidon/microprofile/testing/HelidonTestConfigTest.java b/microprofile/testing/testing/src/test/java/io/helidon/microprofile/testing/HelidonTestConfigTest.java new file mode 100644 index 00000000000..92117ba0663 --- /dev/null +++ b/microprofile/testing/testing/src/test/java/io/helidon/microprofile/testing/HelidonTestConfigTest.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2025 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.testing; + +import io.helidon.config.Config; +import io.helidon.config.mp.MpConfig; + +import org.eclipse.microprofile.config.spi.ConfigProviderResolver; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +/** + * Tests {@link HelidonTestConfig}. + */ +class HelidonTestConfigTest { + + @Test + void testResolve() { + // install the "original" config + ConfigProviderResolver resolver = ConfigProviderResolver.instance(); + resolver.registerConfig(resolver.getBuilder().build(), getClass().getClassLoader()); + + HelidonTestConfig config = new HelidonTestConfig(HelidonTestInfo.classInfo(getClass())); + config.synthetic().update(addConfig("key1", "value1")); + config.synthetic().update(addConfig("key2", "value2")); + + // synthetic config does not resolve + assertThat(config.getOptionalValue("key1", String.class).isPresent(), is(false)); + assertThat(config.getOptionalValue("key2", String.class).isPresent(), is(false)); + + // switch to synthetic + config.resolve(); + + assertThat(config.getValue("key1", String.class), is("value1")); + assertThat(config.getValue("key2", String.class), is("value2")); + + // update synthetic config + assertThat(config.getOptionalValue("complex.key1", String.class).isPresent(), is(false)); + config.synthetic().update(configuration(false, "test-config.yaml")); + assertThat(config.getValue("complex.key1", String.class), is("complex-value1")); + + // useExisting=true + config.synthetic().update(configuration(true)); + + // switch to original + config.resolve(); + + // synthetic config does not resolve + assertThat(config.getOptionalValue("key1", String.class).isPresent(), is(false)); + assertThat(config.getOptionalValue("key2", String.class).isPresent(), is(false)); + assertThat(config.getOptionalValue("complex.key1", String.class).isPresent(), is(false)); + } + + @Test + void testJustInTime() { + HelidonTestConfig config = new HelidonTestConfig(HelidonTestInfo.classInfo(getClass())); + config.resolve(); + + // se config + Config hc = MpConfig.toHelidonConfig(config); + + config.synthetic().update(addConfig("key1", "value1")); + config.synthetic().update(addConfig("key2", "value2")); + assertThat(hc.get("key1").asString().get(), is("value1")); + assertThat(hc.get("key2").asString().get(), is("value2")); + + // update synthetic config + config.synthetic().update(configuration(false, "test-config.yaml")); + assertThat(hc.get("complex.key1").asString().get(), is("complex-value1")); + } + + private static AddConfig addConfig(String key, String value) { + return Proxies.annotation(AddConfig.class, attr -> switch (attr) { + case "key" -> key; + case "value" -> value; + default -> null; + }); + } + + private static Configuration configuration(boolean useExisting, String... configSources) { + return Proxies.annotation(Configuration.class, attr -> switch (attr) { + case "useExisting" -> useExisting; + case "configSources" -> configSources; + default -> null; + }); + } +} diff --git a/microprofile/testing/testing/src/test/resources/test-config.yaml b/microprofile/testing/testing/src/test/resources/test-config.yaml new file mode 100644 index 00000000000..f000a4373e5 --- /dev/null +++ b/microprofile/testing/testing/src/test/resources/test-config.yaml @@ -0,0 +1,18 @@ +# +# Copyright (c) 2024, 2025 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. +# + +complex: + key1: "complex-value1" diff --git a/microprofile/testing/testng/pom.xml b/microprofile/testing/testng/pom.xml index 1d52db60dbf..f8c6b33de60 100644 --- a/microprofile/testing/testng/pom.xml +++ b/microprofile/testing/testng/pom.xml @@ -34,13 +34,13 @@ - io.helidon.microprofile.server - helidon-microprofile-server - true + io.helidon.microprofile.testing + helidon-microprofile-testing - io.helidon.common.testing - helidon-common-testing-virtual-threads + io.helidon.microprofile.server + helidon-microprofile-server + provided io.helidon.microprofile.cdi @@ -50,7 +50,7 @@ org.glassfish.jersey.ext.cdi jersey-weld2-se - true + provided io.helidon.jersey @@ -65,6 +65,10 @@ testng provided + + com.google.inject + guice + 5.1.0 + - - \ No newline at end of file + diff --git a/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/AddBean.java b/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/AddBean.java index 220f1c893cb..14ea4f19c30 100644 --- a/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/AddBean.java +++ b/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/AddBean.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2022, 2025 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. @@ -17,37 +17,45 @@ import java.lang.annotation.Annotation; import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import jakarta.enterprise.context.ApplicationScoped; - /** - * Add a bean. - * This is intended for test sources where we do not want to add {@code beans.xml} as this would add - * all test classes as beans. - * The bean will be added by default with {@link jakarta.enterprise.context.ApplicationScoped}. - * The class will be instantiated using CDI and will be available for injection into test classes and other beans. + * Add a CDI bean to the container. + *

* This annotation can be repeated. + *

+ * If used on a method, the container will be reset regardless of the test lifecycle. + *

+ * The bean scope is defined as follows: + *

    + *
  • If a scope is set with {@link #value()}, it overrides any scope defined on the bean
  • + *
  • Otherwise, the scope defined on the bean is used
  • + *
  • If the bean does not define a scope, {@link jakarta.enterprise.context.ApplicationScoped ApplicationScoped} + * is used
  • + *
+ * @deprecated Use {@link io.helidon.microprofile.testing.AddBean} instead */ +@Inherited @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.METHOD}) @Repeatable(AddBeans.class) +@Deprecated(since = "4.2.0") public @interface AddBean { /** - * Class of the bean. - * @return the class of a bean + * The bean class. + * + * @return bean class */ Class value(); /** - * Scope of the bean. - * Only {@link jakarta.inject.Singleton}, {@link jakarta.enterprise.context.ApplicationScoped} - * and {@link jakarta.enterprise.context.RequestScoped} scopes are supported. + * Override the bean scope. * - * @return scope of the bean + * @return scope class */ - Class scope() default ApplicationScoped.class; + Class scope() default Annotation.class; } diff --git a/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/AddBeans.java b/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/AddBeans.java index 0de9a00f299..2ab89f1a31d 100644 --- a/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/AddBeans.java +++ b/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/AddBeans.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2023 Oracle and/or its affiliates. + * Copyright (c) 2020, 2025 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. @@ -16,21 +16,34 @@ package io.helidon.microprofile.testing.testng; import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * A repeatable container for {@link AddBean}. - * No need to use this annotation, just repeat {@link AddBean} annotation - * on test class. + *

+ * This annotation is optional, you can instead repeat {@link AddBean}. + *

+ * E.g. + *

+ * @AddBean(FooBean.class)
+ * @AddBean(BarBean.class)
+ * class MyTest {
+ * }
+ * 
+ * @deprecated Use {@link io.helidon.microprofile.testing.AddBeans} instead */ +@Inherited @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.METHOD}) +@Deprecated(since = "4.2.0") public @interface AddBeans { /** - * Beans to be added. - * @return add bean annotations + * Get the contained annotations. + * + * @return annotations */ AddBean[] value(); } diff --git a/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/AddConfig.java b/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/AddConfig.java index d1bf335bb5b..0b0cc8e8cc1 100644 --- a/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/AddConfig.java +++ b/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/AddConfig.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2023 Oracle and/or its affiliates. + * Copyright (c) 2020, 2025 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. @@ -16,27 +16,40 @@ package io.helidon.microprofile.testing.testng; import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** - * Add a configuration key/value pair to MicroProfile configuration. + * Add a configuration key/value pair to the {@link Configuration#useExisting() synthetic test configuration}. + *

* This annotation can be repeated. + *

+ * If used on a method, the container will be reset regardless of the test lifecycle. + * + * @see AddConfigs + * @see AddConfigBlock + * @see Configuration + * @deprecated Use {@link io.helidon.microprofile.testing.AddConfig} instead */ +@Inherited @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.METHOD}) @Repeatable(AddConfigs.class) +@Deprecated(since = "4.2.0") public @interface AddConfig { /** * Configuration property key. + * * @return key */ String key(); /** * Configuration property value. + * * @return value */ String value(); diff --git a/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/AddConfigBlock.java b/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/AddConfigBlock.java index 3ba558d2fc3..fd59c41df54 100644 --- a/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/AddConfigBlock.java +++ b/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/AddConfigBlock.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 Oracle and/or its affiliates. + * Copyright (c) 2024, 2025 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. @@ -23,28 +23,35 @@ import java.lang.annotation.Target; /** - * Defines the configuration as a String in {@link #value()} for the - * given type. + * Add a configuration fragment to the {@link Configuration#useExisting() synthetic test configuration}. + *

+ * This annotation can be repeated. + *

+ * If used on a method, the container will be reset regardless of the test lifecycle. + * + * @see AddConfig + * @see AddConfigs + * @see Configuration + * @deprecated Use {@link io.helidon.microprofile.testing.AddConfigBlock} instead */ +@Inherited @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.METHOD}) -@Inherited +@Deprecated(since = "4.2.0") public @interface AddConfigBlock { - /** - * Specifies the format type of the {@link #value()}. - * - * It defaults to 'properties'. + * Specifies the configuration format. + *

+ * The default format is 'properties' * * @return the supported type */ String type() default "properties"; /** - * Configuration value. + * Configuration fragment. * - * @return String with value. + * @return fragment */ String value(); - } diff --git a/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/AddConfigs.java b/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/AddConfigs.java index 4ff9a87c0ee..6b75dbe7f40 100644 --- a/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/AddConfigs.java +++ b/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/AddConfigs.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2023 Oracle and/or its affiliates. + * Copyright (c) 2020, 2025 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. @@ -16,22 +16,37 @@ package io.helidon.microprofile.testing.testng; import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * A repeatable container for {@link AddConfig}. - * No need to use this annotation, just repeat {@link AddConfig} annotation - * on test class. + *

+ * This annotation is optional, you can instead repeat {@link AddConfig}. + *

+ * E.g. + *

+ * @AddConfig(key="foo", value="1")
+ * @AddConfig(key="bar", value="2")
+ * class MyTest {
+ * }
+ * 
+ * + * @see AddConfig + * @see Configuration + * @deprecated Use {@link io.helidon.microprofile.testing.AddConfigs} instead */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.METHOD}) +@Inherited +@Deprecated(since = "4.2.0") public @interface AddConfigs { /** - * Configuration properties to be added. + * Get the contained annotations. * - * @return properties + * @return annotations */ AddConfig[] value(); } diff --git a/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/AddExtension.java b/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/AddExtension.java index 85d246eb01b..8c1d8849f87 100644 --- a/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/AddExtension.java +++ b/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/AddExtension.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2022, 2025 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. @@ -16,6 +16,7 @@ package io.helidon.microprofile.testing.testng; import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -24,15 +25,22 @@ import jakarta.enterprise.inject.spi.Extension; /** - * Add a CDI extension to the test container. + * Add a CDI extension to the container. + *

* This annotation can be repeated. + *

+ * If used on a method, the container will be reset regardless of the test lifecycle. + * @deprecated Use {@link io.helidon.microprofile.testing.AddExtension} instead */ +@Inherited @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.METHOD}) @Repeatable(AddExtensions.class) +@Deprecated(since = "4.2.0") public @interface AddExtension { /** * Class of the extension to add. The class must be public. + * * @return extension class. */ Class value(); diff --git a/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/AddExtensions.java b/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/AddExtensions.java index 02f0eb74d14..f6776846a8f 100644 --- a/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/AddExtensions.java +++ b/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/AddExtensions.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2023 Oracle and/or its affiliates. + * Copyright (c) 2020, 2025 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. @@ -16,21 +16,34 @@ package io.helidon.microprofile.testing.testng; import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * A repeatable container for {@link AddExtension}. - * No need to use this annotation, just repeat {@link AddExtension} annotation - * on test class. + *

+ * This annotation is optional, you can instead repeat {@link AddExtension}. + *

+ * E.g. + *

+ * @AddExtension(FooExtension.class)
+ * @AddExtension(BarExtension.class)
+ * class MyTest {
+ * }
+ * 
+ * @deprecated Use {@link io.helidon.microprofile.testing.AddExtensions} instead */ +@Inherited @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.METHOD}) +@Deprecated(since = "4.2.0") public @interface AddExtensions { /** - * Extensions to be added. - * @return extensions + * Get the contained annotations. + * + * @return annotations */ AddExtension[] value(); } diff --git a/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/AddJaxRs.java b/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/AddJaxRs.java index 250d2fb192e..707d5d550f7 100644 --- a/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/AddJaxRs.java +++ b/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/AddJaxRs.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2025 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. @@ -16,14 +16,32 @@ package io.helidon.microprofile.testing.testng; import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import io.helidon.microprofile.server.JaxRsCdiExtension; +import io.helidon.microprofile.server.ServerCdiExtension; + +import org.glassfish.jersey.ext.cdi1x.internal.CdiComponentProvider; +import org.glassfish.jersey.ext.cdi1x.internal.ProcessAllAnnotatedTypes; +import org.glassfish.jersey.weld.se.WeldRequestScope; + /** - * Add JaxRS support for Request-scoped beans. + * Add JAX-RS (Jersey) support. + *

+ * If used on a method, the container will be reset regardless of the test lifecycle. + * @deprecated Use {@link io.helidon.microprofile.testing.AddJaxRs} instead */ +@Inherited @Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.TYPE}) +@Target({ElementType.TYPE, ElementType.METHOD}) +@AddExtension(ProcessAllAnnotatedTypes.class) +@AddExtension(ServerCdiExtension.class) +@AddExtension(JaxRsCdiExtension.class) +@AddExtension(CdiComponentProvider.class) +@AddBean(WeldRequestScope.class) +@Deprecated(since = "4.2.0") public @interface AddJaxRs { } diff --git a/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/ClassContext.java b/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/ClassContext.java new file mode 100644 index 00000000000..84a6879aec7 --- /dev/null +++ b/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/ClassContext.java @@ -0,0 +1,223 @@ +/* + * Copyright (c) 2025 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.testing.testng; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Semaphore; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import io.helidon.microprofile.testing.HelidonTestInfo; +import io.helidon.microprofile.testing.HelidonTestInfo.ClassInfo; +import io.helidon.microprofile.testing.HelidonTestInfo.MethodInfo; + +import org.testng.ITestClass; +import org.testng.ITestNGMethod; + +/** + * Class context. + * Supports synchronization and ordering of {@link HelidonTestNgListenerBase}. + */ +class ClassContext { + + private final Semaphore semaphore; + private final HelidonTestNgListenerBase listener; + private final ClassInfo classInfo; + private final List methods; + private final AtomicInteger invocationCount = new AtomicInteger(); + private final AtomicBoolean afterClass = new AtomicBoolean(); + private final Map, List> graph = new ConcurrentHashMap<>(); + private final Map> methodsFutures = new ConcurrentHashMap<>(); + + /** + * Create a new instance. + * + * @param tc test class + * @param semaphore class semaphore + * @param listener listener + */ + ClassContext(ITestClass tc, Semaphore semaphore, HelidonTestNgListenerBase listener) { + this.listener = listener; + this.semaphore = semaphore; + this.classInfo = classInfo(tc.getRealClass()); + this.methods = methodInfos(classInfo, + tc.getTestMethods(), + tc.getBeforeTestMethods(), + tc.getBeforeTestMethods(), + tc.getBeforeClassMethods(), + tc.getAfterClassMethods()); + } + + /** + * Wait for the running methods. + */ + void awaitMethods() { + try { + for (CompletableFuture future : Set.copyOf(methodsFutures.values())) { + future.get(); + } + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + } + + /** + * Process a before-invocation event. + * + * @param invokedMethod invoked method + * @param testMethod corresponding test method, may be {@code null} + * @see org.testng.IInvokedMethodListener#beforeInvocation(org.testng.IInvokedMethod, org.testng.ITestResult) + * @see org.testng.IConfigurationListener#beforeConfiguration(org.testng.ITestResult, org.testng.ITestNGMethod) + */ + void beforeInvocation(ITestNGMethod invokedMethod, ITestNGMethod testMethod) { + MethodInfo methodInfo = methodInfo(classInfo, realMethod(invokedMethod)); + HelidonTestInfo testInfo = testMethod != null ? methodInfo(classInfo, realMethod(testMethod)) : classInfo; + graph.compute(testInfo, (k, v) -> { + if (v == null) { + v = new ArrayList<>(); + } + v.add(methodInfo); + return v; + }); + listener.onBeforeInvocation(this, methodInfo, testInfo); + if (invokedMethod.isTest()) { + methodsFutures.put(methodInfo, new CompletableFuture<>()); + } + } + + /** + * Process an after-invocation event. + * + * @param invokedMethod invoked method + * @param testMethod corresponding test method, may be {@code null} + * @see org.testng.IInvokedMethodListener#afterInvocation(org.testng.IInvokedMethod, org.testng.ITestResult) + * @see org.testng.IConfigurationListener#onConfigurationSuccess(org.testng.ITestResult, org.testng.ITestNGMethod) + * @see org.testng.IConfigurationListener#onConfigurationFailure(org.testng.ITestResult, org.testng.ITestNGMethod) + */ + void afterInvocation(ITestNGMethod invokedMethod, ITestNGMethod testMethod) { + try { + MethodInfo methodInfo = methodInfo(classInfo, realMethod(invokedMethod)); + HelidonTestInfo testInfo = testMethod != null ? methodInfo(classInfo, realMethod(testMethod)) : classInfo; + List deps = graph.compute(testInfo, (k, v) -> { + if (v == null) { + v = new ArrayList<>(); + } + v.remove(methodInfo); + return v; + }); + boolean last = deps.isEmpty(); + listener.onAfterInvocation(methodInfo, testInfo, last); + CompletableFuture future = methodsFutures.get(methodInfo); + if (future != null) { + future.complete(null); + } + } finally { + afterClass(AtomicInteger::incrementAndGet); + } + } + + /** + * Process a before-class event. + * + * @see org.testng.IClassListener#onBeforeClass(org.testng.ITestClass) + */ + void beforeClass() { + try { + semaphore.acquire(); + listener.onBeforeClass(classInfo); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + /** + * Process an after-class event. + * + * @see org.testng.IClassListener#onAfterClass(org.testng.ITestClass) + */ + void afterClass() { + afterClass(AtomicInteger::get); + } + + @Override + public String toString() { + return "ClassContext{" + + "class=" + classInfo.element().getName() + + ", methods: " + methodNames(methods) + + '}'; + } + + private void afterClass(Function op) { + if (op.apply(invocationCount) == methods.size() + && afterClass.compareAndSet(false, true)) { + try { + listener.onAfterClass(classInfo); + methodsFutures.clear(); + } finally { + semaphore.release(); + } + } + } + + /** + * Get a class info. + * + * @param cls class + * @return ClassInfo + */ + static ClassInfo classInfo(Class cls) { + return HelidonTestInfo.classInfo(cls, HelidonTestDescriptorImpl::new); + } + + /** + * Get a method info. + * + * @param classInfo class info + * @param method method + * @return MethodInfo + */ + static MethodInfo methodInfo(ClassInfo classInfo, Method method) { + return HelidonTestInfo.methodInfo(method, classInfo, HelidonTestDescriptorImpl::new); + } + + private static List methodInfos(ClassInfo classInfo, ITestNGMethod[]... methods) { + return Stream.of(methods) + .flatMap(Stream::of) + .map(m -> methodInfo(classInfo, realMethod(m))) + .toList(); + } + + private static String methodNames(List methodInfos) { + return methodInfos.stream() + .map(MethodInfo::element) + .map(Method::getName) + .collect(Collectors.joining(",", "[", "]")); + } + + private static Method realMethod(ITestNGMethod tm) { + return tm.getConstructorOrMethod().getMethod(); + } +} diff --git a/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/ClassDecorator.java b/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/ClassDecorator.java new file mode 100644 index 00000000000..4828c74592d --- /dev/null +++ b/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/ClassDecorator.java @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2025 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.testing.testng; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.testng.ITestClass; +import org.testng.ITestNGMethod; +import org.testng.internal.ITestClassConfigInfo; +import org.testng.xml.XmlClass; +import org.testng.xml.XmlTest; + +import static io.helidon.microprofile.testing.Instrumented.isInstrumented; + +/** + * A decorator for {@link ITestClass} that is used to customize the value of {@link ITestClass#getName()}. + *

    + *
  • It is used to hide the instrumented name in the test results
  • + *
  • It is installed on every method using {@link ITestNGMethod#setTestClass(ITestClass)}
  • + *
+ * + * @param delegate delegate + */ +record ClassDecorator(ITestClass delegate) implements ITestClass, ITestClassConfigInfo { + + private static final Map CACHE = new ConcurrentHashMap<>(); + + /** + * Decorate the given test class. + * + * @param tc test class + * @return decorated test class + */ + static ITestClass decorate(ITestClass tc) { + if (!(tc instanceof ClassDecorator)) { + return CACHE.computeIfAbsent(tc, ClassDecorator::new); + } + return tc; + } + + @Override + public List getAllBeforeClassMethods() { + if (delegate instanceof ITestClassConfigInfo info) { + return info.getAllBeforeClassMethods().stream() + .peek(m -> m.setTestClass(decorate(m.getTestClass()))) + .toList(); + } + return List.of(); + } + + @Override + public List getInstanceBeforeClassMethods(Object instance) { + if (delegate instanceof ITestClassConfigInfo info) { + return info.getInstanceBeforeClassMethods(instance).stream() + .peek(m -> m.setTestClass(decorate(m.getTestClass()))) + .toList(); + } + return List.of(); + } + + @Override + public String getName() { + Class realClass = getRealClass(); + return isInstrumented(realClass) ? realClass.getSuperclass().getName() : realClass.getName(); + } + + @Override + public Class getRealClass() { + return delegate.getRealClass(); + } + + private ITestNGMethod[] processMethods(ITestNGMethod[] methods) { + for (ITestNGMethod method : methods) { + method.setTestClass(decorate(method.getTestClass())); + } + return methods; + } + + @Override + public ITestNGMethod[] getAfterClassMethods() { + return processMethods(delegate.getAfterClassMethods()); + } + + @Override + public ITestNGMethod[] getAfterGroupsMethods() { + return processMethods(delegate.getAfterGroupsMethods()); + } + + @Override + public ITestNGMethod[] getAfterSuiteMethods() { + return processMethods(delegate.getAfterSuiteMethods()); + } + + @Override + public ITestNGMethod[] getAfterTestConfigurationMethods() { + return processMethods(delegate.getAfterTestConfigurationMethods()); + } + + @Override + public ITestNGMethod[] getAfterTestMethods() { + return processMethods(delegate.getAfterTestMethods()); + } + + @Override + public ITestNGMethod[] getBeforeClassMethods() { + return processMethods(delegate.getBeforeClassMethods()); + } + + @Override + public ITestNGMethod[] getBeforeGroupsMethods() { + return processMethods(delegate.getBeforeGroupsMethods()); + } + + @Override + public ITestNGMethod[] getBeforeSuiteMethods() { + return processMethods(delegate.getBeforeSuiteMethods()); + } + + @Override + public ITestNGMethod[] getBeforeTestConfigurationMethods() { + return processMethods(delegate.getBeforeTestConfigurationMethods()); + } + + @Override + public ITestNGMethod[] getBeforeTestMethods() { + return processMethods(delegate.getBeforeTestMethods()); + } + + @Override + public ITestNGMethod[] getTestMethods() { + return processMethods(delegate.getTestMethods()); + } + + @Override + public void addInstance(Object instance) { + delegate.addInstance(instance); + } + + @Override + public long[] getInstanceHashCodes() { + return delegate.getInstanceHashCodes(); + } + + @Override + public Object[] getInstances(boolean create) { + return delegate.getInstances(create); + } + + @Override + public Object[] getInstances(boolean create, String errorMsgPrefix) { + return delegate.getInstances(create, errorMsgPrefix); + } + + @Override + public String getTestName() { + return delegate.getTestName(); + } + + @Override + public XmlClass getXmlClass() { + return delegate.getXmlClass(); + } + + @Override + public XmlTest getXmlTest() { + return delegate.getXmlTest(); + } +} diff --git a/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/Configuration.java b/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/Configuration.java index 23a60e70247..16e50b03080 100644 --- a/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/Configuration.java +++ b/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/Configuration.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2022, 2025 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. @@ -23,36 +23,55 @@ import java.lang.annotation.Target; /** - * Additional configuration of config itself. + * General setting for the test configuration. + *

+ * If used on a method, the container will be reset regardless of the test lifecycle. + * + * @see AddConfig + * @see AddConfigs + * @see AddConfigBlock + * @deprecated Use {@link io.helidon.microprofile.testing.Configuration} instead */ +@Inherited @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.METHOD}) -@Inherited +@Deprecated(since = "4.2.0") public @interface Configuration { /** - * If set to {@code true}, the existing (or default) MicroProfile configuration would be used. - * By default uses a configuration constructed using all {@link AddConfig} - * annotations and {@link #configSources()}. - * When set to false and a {@link org.testng.annotations.BeforeClass} method registers a custom configuration - * with {@link org.eclipse.microprofile.config.spi.ConfigProviderResolver}, the result is undefined, though - * tests have shown that the registered config may be used (as BeforeAll ordering is undefined by - * JUnit, it may be called after our extension) + * If set to {@code false}, the synthetic test configuration is used. + *

+ * The synthetic test configuration is expressed with the following: + *

    + *
  • {@link #configSources()}
  • + *
  • {@link #profile()}
  • + *
  • {@link AddConfig}
  • + *
  • {@link AddConfigs}
  • + *
  • {@link AddConfigBlock}
  • + *
+ *

+ * If set to {@code true}, only the existing (or default) MicroProfile configuration is used + * and the annotations listed previously are ignored. + *

+ * You can use {@link org.eclipse.microprofile.config.spi.ConfigProviderResolver ConfigProviderResolver} to define + * the configuration programmatically before the CDI container starts. * - * @return whether to use existing (or default) configuration, or customized one + * @return whether to use existing (or default) configuration */ boolean useExisting() default false; /** - * Class path properties config sources to add to configuration of this test class or method. + * Class-path resources to add as config sources to the synthetic test configuration. * - * @return config sources to add + * @return config sources */ String[] configSources() default {}; /** * Configuration profile. + *

+ * The default profile is 'test' * - * @return String with default value "test". + * @return profile */ String profile() default "test"; } diff --git a/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/DisableDiscovery.java b/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/DisableDiscovery.java index a4b66811f6e..471ab843795 100644 --- a/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/DisableDiscovery.java +++ b/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/DisableDiscovery.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2023 Oracle and/or its affiliates. + * Copyright (c) 2020, 2025 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. @@ -23,30 +23,39 @@ import java.lang.annotation.Target; /** - * Whether discovery is automated or disabled. If discovery is desired, do not annotate test - * class with this annotation. + * Disables CDI discovery. *

- * When discovery is enabled, the whole classpath is scanned for bean archives (jar files containing - * {@code META-INF/beans.xml}) and all beans and extensions are added automatically. + * If discovery is desired, do not annotate test class with this annotation. *

- * When discovery is disabled, CDI would only contain the CDI implementation itself and beans and extensions added - * through annotations {@link AddBean} and - * {@link AddExtension} - * - * If discovery is disabled on class level and desired on method level, - * the value can be set to {@code false}. + * If used on a method, the container will be reset regardless of the test lifecycle. + *

+ * When disabling discovery, you are responsible for adding the beans and extensions needed to activate the features you need. + * You can use the following annotations to do that: + *

    + *
  • {@link AddBean} to add CDI beans
  • + *
  • {@link AddExtension} to add CDI extensions
  • + *
  • {@link AddJaxRs} a shorthand to add JAX-RS (Jersey)
  • + *
+ *

+ * See also the following "core" CDI extensions: + *

    + *
  • {@link io.helidon.microprofile.server.ServerCdiExtension ServerCdiExtension} optional if using {@link AddJaxRs}
  • + *
  • {@link io.helidon.microprofile.server.JaxRsCdiExtension JaxRsCdiExtension} optional if using {@link AddJaxRs}
  • + *
  • {@link io.helidon.microprofile.config.ConfigCdiExtension ConfigCdiExtension}
  • + *
*/ +@Inherited @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.METHOD}) -@Inherited +@Deprecated(since = "4.2.0") public @interface DisableDiscovery { /** - * By default if you annotate a class or a method, discovery gets disabled. + * By default, if you annotate a class or a method, discovery gets disabled. * If you want to override configuration on method to differ from class, you * can configure the value to {@code false}, effectively enabling discovery. * * @return whether to disable discovery ({@code true}), or enable it ({@code false}). If this - * annotation is not present, discovery is enabled + * annotation is not present, discovery is enabled */ boolean value() default true; } diff --git a/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/HelidonTest.java b/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/HelidonTest.java index 2f3ef14400f..3f810612ac9 100644 --- a/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/HelidonTest.java +++ b/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/HelidonTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2024 Oracle and/or its affiliates. + * Copyright (c) 2022, 2025 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. @@ -24,30 +24,18 @@ import static io.helidon.common.testing.virtualthreads.PinningRecorder.DEFAULT_THRESHOLD; /** - * An annotation making this test class a CDI bean with support for injection. - *

- * There is no need to provide {@code beans.xml} (actually it is not recommended, as it would combine beans - * from all tests), instead use {@link AddBean}, - * {@link AddExtension}, and {@link AddConfig} - * annotations to control the shape of the container. - *

+ * This extension starts a CDI container and adds the test class as a bean with support for injection. + *

+ * The container is started lazily during test execution to ensure that it is started after all other extensions. + *

+ * The container can be customized with the following annotations: + *

+ *

+ * The configuration can be customized with the following annotations: + *

    + *
  • {@link io.helidon.microprofile.testing.Configuration} global setting for MicroProfile configuration
  • + *
  • {@link io.helidon.microprofile.testing.AddConfig} declarative key/value pair configuration
  • + *
  • {@link io.helidon.microprofile.testing.AddConfigBlock} declarative fragment configuration
  • + *
+ *

+ * See also {@link io.helidon.microprofile.testing.Socket}, a CDI qualifier to inject JAX-RS client or URI. + *

+ * The container is created per test class by default, unless + * {@link HelidonTest#resetPerTest()} is {@code true}, in + * which case the container is created per test method. + *

+ * The container and the configuration can be customized per method regardless of the value of + * {@link HelidonTest#resetPerTest()}. The container will be reset accordingly. + *

+ * It is not recommended to provide a {@code beans.xml} along the test classes, as it would combine beans from all tests. + * Instead, you should use {@link io.helidon.microprofile.testing.AddBean} to specify the beans per test or method. + * + * @see HelidonTest */ -public class HelidonTestNgListener implements IClassListener, ITestListener { - - 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, - Singleton.class, ApplicationScoped.Literal.INSTANCE, - RequestScoped.class, RequestScoped.Literal.INSTANCE, - Dependent.class, Dependent.Literal.INSTANCE); - - private List classLevelExtensions = new ArrayList<>(); - private List classLevelBeans = new ArrayList<>(); - private ConfigMeta classLevelConfigMeta = new ConfigMeta(); - private boolean classLevelDisableDiscovery = false; - private boolean helidonTest; - private boolean resetPerTest; - - private Class testClass; - private Object testInstance; - private ConfigProviderResolver configProviderResolver; - private Config config; - private SeContainer container; - private PinningRecorder pinningRecorder; - - @Override - public void onBeforeClass(ITestClass iTestClass) { - testClass = iTestClass.getRealClass(); - HelidonTest testAnnot = testClass.getAnnotation(HelidonTest.class); - if (testAnnot == null) { - helidonTest = false; - return; - } - helidonTest = true; - resetPerTest = testAnnot.resetPerTest(); - - List metaAnnotations = extractMetaAnnotations(testClass); - - AddConfig[] configs = getAnnotations(testClass, AddConfig.class, metaAnnotations); - - classLevelConfigMeta.addConfig(configs); - classLevelConfigMeta.configuration(getAnnotation(testClass, Configuration.class, metaAnnotations)); - classLevelConfigMeta.addConfigBlock(getAnnotation(testClass, AddConfigBlock.class, metaAnnotations)); - configProviderResolver = ConfigProviderResolver.instance(); - - AddExtension[] extensions = getAnnotations(testClass, AddExtension.class, metaAnnotations); - classLevelExtensions.addAll(Arrays.asList(extensions)); - - AddBean[] beans = getAnnotations(testClass, AddBean.class, metaAnnotations); - classLevelBeans.addAll(Arrays.asList(beans)); - - if (testAnnot.pinningDetection()) { - pinningRecorder = PinningRecorder.create(); - pinningRecorder.record(Duration.ofMillis(testAnnot.pinningThreshold())); - } - - DisableDiscovery discovery = getAnnotation(testClass, DisableDiscovery.class, metaAnnotations); - if (discovery != null) { - classLevelDisableDiscovery = discovery.value(); - } - - if (resetPerTest) { - validatePerTest(); - return; - } - validatePerClass(); - - // add beans when using JaxRS - AddJaxRs addJaxRsAnnotation = getAnnotation(testClass, AddJaxRs.class, metaAnnotations); - if (addJaxRsAnnotation != null){ - classLevelExtensions.add(ProcessAllAnnotatedTypesLiteral.INSTANCE); - classLevelExtensions.add(ServerCdiExtensionLiteral.INSTANCE); - classLevelExtensions.add(JaxRsCdiExtensionLiteral.INSTANCE); - classLevelExtensions.add(CdiComponentProviderLiteral.INSTANCE); - classLevelBeans.add(WeldRequestScopeLiteral.INSTANCE); - } +public class HelidonTestNgListener extends HelidonTestNgListenerBase implements ITestListener, + ISuiteListener, + IAlterSuiteListener, + IMethodInterceptor { - configure(classLevelConfigMeta); + private static final Logger LOGGER = System.getLogger(HelidonTestNgListener.class.getName()); - Object[] testInstances = iTestClass.getInstances(false); - if (testInstances != null && testInstances.length > 0) { - testInstance = testInstances[0]; - } + private static final List TYPE_ANNOTATIONS = List.of( + Proxies.annotation(Guice.class, attr -> { + if (attr.equals("moduleFactory")) { + return HelidonTestNgModuleFactory.class; + } + return null; + })); - if (!classLevelConfigMeta.useExisting) { - startContainer(classLevelBeans, classLevelExtensions, classLevelDisableDiscovery); - } - } + private static final List> METHOD_EXCLUDES = List.of( + BeforeTest.class, + BeforeSuite.class); + private final Map, Context> staticContexts = new ConcurrentHashMap<>(); + private final Semaphore semaphore = new Semaphore(1); + private volatile HelidonTestContainer container; @Override - public void onAfterClass(ITestClass testClass) { - if (!helidonTest) { - return; - } - - if (!resetPerTest) { - releaseConfig(); - stopContainer(); - } - if (pinningRecorder != null) { - pinningRecorder.close(); - pinningRecorder = null; + public void alter(List suites) { + for (XmlSuite suite : suites) { + for (XmlTest test : suite.getTests()) { + Set xmlClasses = new HashSet<>(test.getClasses()); + for (XmlPackage xmlPackage : test.getXmlPackages()) { + xmlClasses.addAll(xmlPackage.getXmlClasses()); + } + for (XmlClass xmlClass : xmlClasses) { + ClassInfo classInfo = classInfo(xmlClass.getSupportClass()); + if (classInfo.containsAnnotation(HelidonTest.class)) { + Class testClass = classInfo.element(); + if (Modifier.isAbstract(testClass.getModifiers())) { + continue; + } + if (Modifier.isFinal(testClass.getModifiers())) { + LOGGER.log(Level.WARNING, "Cannot instrument final class: {0}", testClass.getName()); + continue; + } + Context staticContext = staticContexts.computeIfAbsent(testClass, this::staticContext); + xmlClass.setClass(Contexts.runInContext(staticContext, () -> { + // Instrument the test class + // Add a @Guice annotation to install HelidonTestNgModuleFactory + // Use a proxy to start the container lazily + return (Class) instrument(testClass, TYPE_ANNOTATIONS, METHOD_EXCLUDES, this::invoke); + })); + } + } + } } } @Override - public void onTestStart(ITestResult result) { - if (!helidonTest) { - return; - } - - if (resetPerTest) { - Method method = result.getMethod().getConstructorOrMethod().getMethod(); - AddConfig[] configs = method.getAnnotationsByType(AddConfig.class); - ConfigMeta methodLevelConfigMeta = classLevelConfigMeta.nextMethod(); - methodLevelConfigMeta.addConfig(configs); - methodLevelConfigMeta.configuration(method.getAnnotation(Configuration.class)); - methodLevelConfigMeta.addConfigBlock(method.getAnnotation(AddConfigBlock.class)); - - configure(methodLevelConfigMeta); - - List methodLevelExtensions = new ArrayList<>(classLevelExtensions); - List methodLevelBeans = new ArrayList<>(classLevelBeans); - boolean methodLevelDisableDiscovery = classLevelDisableDiscovery; - - AddExtension[] extensions = method.getAnnotationsByType(AddExtension.class); - methodLevelExtensions.addAll(Arrays.asList(extensions)); - - AddBean[] beans = method.getAnnotationsByType(AddBean.class); - methodLevelBeans.addAll(Arrays.asList(beans)); - - DisableDiscovery discovery = method.getAnnotation(DisableDiscovery.class); - if (discovery != null) { - methodLevelDisableDiscovery = discovery.value(); + public void onStart(ISuite suite) { + for (ITestNGMethod tm : suite.getAllMethods()) { + if (isInstrumented(tm.getTestClass().getRealClass())) { + // replace the built-in ITestClass with a decorator + // to hide the instrumented class name in the test results + tm.setTestClass(ClassDecorator.decorate(tm.getTestClass())); } - - startContainer(methodLevelBeans, methodLevelExtensions, methodLevelDisableDiscovery); } } @Override - public void onTestFailure(ITestResult iTestResult) { - if (!helidonTest) { - return; - } - - if (resetPerTest) { - releaseConfig(); - stopContainer(); - } + boolean filterClass(Class cls) { + return isInstrumented(cls); } @Override - public void onTestSuccess(ITestResult iTestResult) { - if (!helidonTest) { - return; - } - - if (resetPerTest) { - releaseConfig(); - stopContainer(); - } - } - - private void validatePerClass() { - validateMethods(testClass.getMethods()); - validateMethods(testClass.getDeclaredMethods()); - validateConstructors(testClass.getDeclaredConstructors()); - AddJaxRs addJaxRsAnnotation = testClass.getAnnotation(AddJaxRs.class); - if (addJaxRsAnnotation != null){ - if (testClass.getAnnotation(DisableDiscovery.class) == null){ - throw new RuntimeException("@AddJaxRs annotation should be used only with @DisableDiscovery annotation."); - } - } - } - - private void validatePerTest() { - Constructor[] constructors = testClass.getConstructors(); - if (constructors.length > 1) { - throw new RuntimeException("When a class is annotated with @HelidonTest(resetPerTest=true)," - + " the class must have only a single no-arg constructor"); - } - if (constructors.length == 1) { - Constructor c = constructors[0]; - if (c.getParameterCount() > 0) { - throw new RuntimeException("When a class is annotated with @HelidonTest(resetPerTest=true)," - + " the class must have a no-arg constructor"); - } - } - validateFields(testClass.getFields()); - validateFields(testClass.getDeclaredFields()); - } - - private void configure(ConfigMeta configMeta) { - if (config != null) { - configProviderResolver.releaseConfig(config); - } - if (!configMeta.useExisting) { - // only create a custom configuration if not provided by test method/class - // prepare configuration - ConfigBuilder builder = configProviderResolver.getBuilder(); - - configMeta.additionalSources.forEach(it -> { - String fileName = it.trim(); - int idx = fileName.lastIndexOf('.'); - String type = idx > -1 ? fileName.substring(idx + 1) : "properties"; - try { - Enumeration urls = Thread.currentThread().getContextClassLoader().getResources(fileName); - urls.asIterator().forEachRemaining(url -> builder.withSources(MpConfigSources.create(type, url))); - } catch (IOException e) { - throw new IllegalStateException("Failed to read \"" + fileName + "\" from classpath", e); - } - }); - if (configMeta.type != null && configMeta.block != null) { - builder.withSources(MpConfigSources.create(configMeta.type, new StringReader(configMeta.block))); - } - config = builder - .withSources(MpConfigSources.create(configMeta.additionalKeys)) - .addDefaultSources() - .addDiscoveredSources() - .addDiscoveredConverters() - .build(); - configProviderResolver.registerConfig(config, Thread.currentThread().getContextClassLoader()); - } - } - - private void releaseConfig() { - if (configProviderResolver != null && config != null) { - configProviderResolver.releaseConfig(config); - config = null; - - classLevelExtensions = new ArrayList<>(); - classLevelBeans = new ArrayList<>(); - classLevelConfigMeta = new ConfigMeta(); - classLevelDisableDiscovery = false; - configProviderResolver = ConfigProviderResolver.instance(); - } - } - - @SuppressWarnings("unchecked") - private void startContainer(List beanAnnotations, - List extensionAnnotations, - boolean disableDiscovery) { - - SeContainerInitializer initializer = SeContainerInitializer.newInstance(); - - if (disableDiscovery) { - initializer.disableDiscovery(); - } - - initializer.addExtensions( - new TestInstanceExtension(testInstance, testClass), - new AddBeansExtension(beanAnnotations)); - - for (AddExtension addExtension : extensionAnnotations) { - Class extensionClass = addExtension.value(); - if (Modifier.isPublic(extensionClass.getModifiers())) { - initializer.addExtensions(addExtension.value()); + public List intercept(List methods, ITestContext context) { + LOGGER.log(Level.DEBUG, () -> "intercept - methods: " + methods.stream() + .map(m -> m.getMethod().getConstructorOrMethod().getMethod()) + .map(m -> m.getDeclaringClass().getName() + "#" + m.getName()) + .collect(joining(","))); + + // group the methods that share the same container + List shared = new ArrayList<>(); + List exclusive = new ArrayList<>(); + for (IMethodInstance e : methods) { + Method method = e.getMethod().getConstructorOrMethod().getMethod(); + ClassInfo classInfo = HelidonTestInfo.classInfo(method.getDeclaringClass(), HelidonTestDescriptorImpl::new); + MethodInfo methodInfo = HelidonTestInfo.methodInfo(method, classInfo, HelidonTestDescriptorImpl::new); + if (!classInfo.resetPerTest() && methodInfo.requiresReset()) { + exclusive.add(e); } else { - throw new IllegalArgumentException("Extension classes must be public, but " + extensionClass - .getName() + " is not"); - } - } - - container = initializer.initialize(); - container.select(testClass).get(); - } - - private void stopContainer() { - if (container != null) { - container.close(); - container = null; - } - } - - 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(Annotation::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); + shared.add(e); } } - return annotation; - } + if (!exclusive.isEmpty()) { + List result = new ArrayList<>(shared); + result.addAll(exclusive); + result.sort(Comparator.comparingInt(e -> e.getMethod().getPriority())); - @SuppressWarnings("unchecked") - 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); + LOGGER.log(Level.DEBUG, () -> "intercept - sorted methods: " + result.stream() + .map(m -> m.getMethod().getConstructorOrMethod().getMethod()) + .map(m -> m.getDeclaringClass().getName() + "#" + m.getName()) + .collect(joining(","))); - List allAnnotations = new ArrayList<>(List.of(directAnnotations)); - // Include meta annotations - allAnnotations.addAll(annotationsByType(annotClass, metaAnnotations)); - - Class superClass = testClass.getSuperclass(); - while (superClass != null) { - directAnnotations = superClass.getAnnotationsByType(annotClass); - allAnnotations.addAll(List.of(directAnnotations)); - superClass = superClass.getSuperclass(); - } - - Object result = Array.newInstance(annotClass, allAnnotations.size()); - for (int i = 0; i < allAnnotations.size(); i++) { - Array.set(result, i, allAnnotations.get(i)); + return result; } - - return (T[]) result; + LOGGER.log(Level.DEBUG, "intercept - methods not modified"); + return methods; } - private static void validateMethods(Method[] methods) { - for (Method method : methods) { - if (method.getAnnotation(Test.class) != null) { - // a test method - if (hasAnnotation(method, TEST_ANNOTATIONS)) { - throw new RuntimeException("When a class is annotated with @HelidonTest, " - + "there is a single CDI container used to invoke all " - + "test methods on the class. Method " + method - + " has an annotation that modifies container behavior."); - } + @Override + void onBeforeInvocation(ClassContext classContext, MethodInfo methodInfo, HelidonTestInfo testInfo) { + LOGGER.log(Level.DEBUG, "onBeforeInvocation: {0}", testInfo.id()); + try { + if (testInfo.requiresReset()) { + semaphore.acquire(); + classContext.awaitMethods(); + closeContainer(testInfo); + initContainer(testInfo); + } else { + semaphore.acquire(); + initContainer(testInfo.classInfo()); + semaphore.release(); } + } catch (InterruptedException e) { + throw new RuntimeException(e); } } - private static void validateConstructors(Constructor[] constructors) { - for (Constructor constructor : constructors) { - if (constructor.getAnnotation(Inject.class) != null) { - if (hasAnnotation(constructor, TEST_ANNOTATIONS)) { - throw new RuntimeException("When a class is annotated with @HelidonTest, " - + "there is a single CDI container used to invoke all " - + "test methods on the class. Do not use @Inject annotation" - + "over constructor. Use it on each field."); - } + @Override + void onAfterInvocation(MethodInfo methodInfo, HelidonTestInfo testInfo, boolean last) { + LOGGER.log(Level.DEBUG, "onAfterInvocation: {0}", methodInfo.id()); + if (last) { + if (testInfo.requiresReset()) { + closeContainer(testInfo); + semaphore.release(); } } } - private static void validateFields(Field[] fields) { - for (Field field : fields) { - if (field.getAnnotation(Inject.class) != null) { - throw new RuntimeException("When a class is annotated with @HelidonTest(resetPerTest=true)," - + " injection into fields or constructor is not supported, as each" - + " test method uses a different CDI container. Field " + field - + " is annotated with @Inject"); - } - } + @Override + void onBeforeClass(ClassInfo classInfo) { + LOGGER.log(Level.DEBUG, "onBeforeClass: {0}", classInfo.id()); } - private static boolean hasAnnotation(AnnotatedElement element, Set> annotations) { - for (Class aClass : annotations) { - if (element.getAnnotation(aClass) != null) { - return true; - } - } - return false; + @Override + void onAfterClass(ClassInfo classInfo) { + LOGGER.log(Level.DEBUG, "onAfterClass: {0}", classInfo.id()); + closeContainer(classInfo); + semaphore.drainPermits(); + semaphore.release(); } - @SuppressWarnings("CdiManagedBeanInconsistencyInspection") - private record TestInstanceExtension(Object testInstance, Class testClass) implements Extension { - - void registerTestClass(@Observes BeforeBeanDiscovery event) { - event.addAnnotatedType(testClass, "testng-" + testClass.getName()) - .add(SingletonLiteral.INSTANCE); - } - - @SuppressWarnings("unchecked") - void registerTestInstances(@Observes ProcessInjectionTarget pit) { - if (pit.getAnnotatedType().getJavaClass().equals(testClass)) { - pit.setInjectionTarget(new TestInjectionTarget<>(pit.getInjectionTarget(), (T) testInstance)); - } + private Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + Class testClass = proxy.getClass().getSuperclass(); + Context staticContext = staticContexts.get(testClass); + if (staticContext == null) { + throw new IllegalStateException("Static context not set"); } - } - - @SuppressWarnings("CdiManagedBeanInconsistencyInspection") - private record AddBeansExtension(List addBeans) implements Extension { - - void registerOtherBeans(@Observes AfterBeanDiscovery event) { - event.addBean() - .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) { - Class beanType = beanDef.value(); - Class scopeType = beanDef.scope(); - - AnnotationLiteral scope = BEAN_DEFINING.get(scopeType); - if (scope == null) { - throw new IllegalStateException( - "Only on of " + BEAN_DEFINING.keySet() + " scopes are allowed in tests. Scope " - + scopeType.getName() + " is not allowed for bean " + beanDef.value().getName()); - } - - AnnotatedTypeConfigurator configurator = event.addAnnotatedType(beanType, "testng-" + beanType.getName()); - if (!hasAnnotation(beanType, BEAN_DEFINING.keySet())) { - configurator.add(scope); + try { + return Contexts.runInContext(staticContext, () -> { + if (container == null) { + throw new IllegalStateException("Container not set"); } - } - } - - @SuppressWarnings("unchecked") - static String clientUri() { - try { - Class extClass = Class.forName("io.helidon.microprofile.server.ServerCdiExtension"); - Extension extension = CDI.current().getBeanManager().getExtension((Class) extClass); - Method m = extension.getClass().getMethod("port"); - int port = (int) m.invoke(extension); - return "http://localhost:" + port; - } catch (ReflectiveOperationException e) { - return "http://localhost:7001"; - } - } - } - - record TestInjectionTarget(InjectionTarget delegate, T testInstance) - implements InjectionTarget { - - @Override - public void dispose(T i) { - } - - @Override - public Set getInjectionPoints() { - return delegate.getInjectionPoints(); - } - - @Override - public void inject(T testInstance, CreationalContext cc) { - delegate.inject(testInstance, cc); - } - - @Override - public void postConstruct(T testInstance) { - delegate.postConstruct(testInstance); + Object instance = container.resolveInstance(testClass); + try { + method.setAccessible(true); + return method.invoke(instance, args); + } catch (InvocationTargetException e) { + if (e.getTargetException() instanceof RuntimeException) { + throw (RuntimeException) e.getTargetException(); + } + throw new UncheckedException(e.getTargetException()); } - - @Override - public void preDestroy(T testInstance) { - delegate.preDestroy(testInstance); - } - - @Override - public T produce(CreationalContext cc) { - return testInstance; - } - } - - private static final class ConfigMeta { - private final Map additionalKeys = new HashMap<>(); - private final List additionalSources = new ArrayList<>(); - private String type; - private String block; - private boolean useExisting; - private String profile; - - ConfigMeta() { - // to allow SeContainerInitializer (forbidden by default because of native image) - additionalKeys.put("mp.initializer.allow", "true"); - additionalKeys.put("mp.initializer.no-warn", "true"); - // to run on random port - additionalKeys.put("server.port", "0"); - // higher ordinal then all the defaults, system props and environment variables - additionalKeys.putIfAbsent(ConfigSource.CONFIG_ORDINAL, "1000"); - // profile - additionalKeys.put("mp.config.profile", "test"); - } - - void addConfig(AddConfig[] configs) { - for (AddConfig config : configs) { - additionalKeys.put(config.key(), config.value()); - } - } - - void configuration(Configuration config) { - if (config == null) { - return; - } - useExisting = config.useExisting(); - profile = config.profile(); - additionalSources.addAll(List.of(config.configSources())); - //set additional key for profile - additionalKeys.put("mp.config.profile", profile); - } - - void addConfigBlock(AddConfigBlock config) { - if (config == null) { - return; - } - this.type = config.type(); - this.block = config.value(); - } - - 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; - } - } - - @SuppressWarnings("ClassExplicitlyAnnotation") - private static final class WeldRequestScopeLiteral extends AnnotationLiteral implements AddBean { - - static final WeldRequestScopeLiteral INSTANCE = new WeldRequestScopeLiteral(); - - @Override - public Class value() { - return org.glassfish.jersey.weld.se.WeldRequestScope.class; - } - - @Override - public Class scope() { - return RequestScoped.class; - } - } - - - @SuppressWarnings("ClassExplicitlyAnnotation") - private static final class ProcessAllAnnotatedTypesLiteral extends AnnotationLiteral implements AddExtension { - - static final ProcessAllAnnotatedTypesLiteral INSTANCE = new ProcessAllAnnotatedTypesLiteral(); - - @Override - public Class value() { - return org.glassfish.jersey.ext.cdi1x.internal.ProcessAllAnnotatedTypes.class; + }); + } catch (UncheckedException e) { + throw e.getCause(); } } - @SuppressWarnings("ClassExplicitlyAnnotation") - private static final class ServerCdiExtensionLiteral extends AnnotationLiteral implements AddExtension { - - static final ServerCdiExtensionLiteral INSTANCE = new ServerCdiExtensionLiteral(); - - @Override - public Class value() { - return ServerCdiExtension.class; + private void initContainer(HelidonTestInfo testInfo) { + if (container == null) { + LOGGER.log(Level.DEBUG, "initContainer: {0}", testInfo.id()); + HelidonTestScope testScope = HelidonTestScope.ofContainer(); + container = new HelidonTestContainer(testInfo, testScope, HelidonTestExtensionImpl::new); } } - @SuppressWarnings("ClassExplicitlyAnnotation") - private static final class JaxRsCdiExtensionLiteral extends AnnotationLiteral implements AddExtension { - - static final JaxRsCdiExtensionLiteral INSTANCE = new JaxRsCdiExtensionLiteral(); - - @Override - public Class value() { - return JaxRsCdiExtension.class; + private void closeContainer(HelidonTestInfo testInfo) { + if (container != null) { + LOGGER.log(Level.DEBUG, "closeContainer: {0}", testInfo.id()); + container.close(); + container = null; } } - @SuppressWarnings("ClassExplicitlyAnnotation") - private static final class CdiComponentProviderLiteral extends AnnotationLiteral implements AddExtension { + private Context staticContext(Class testClass) { + var context = Context.builder() + .id("test-" + testClass.getName() + "-" + System.identityHashCode(testClass)) + .build(); - static final CdiComponentProviderLiteral INSTANCE = new CdiComponentProviderLiteral(); + // self-register, so this context is used even if the current context is some child of it + context.register(GlobalServiceRegistry.STATIC_CONTEXT_CLASSIFIER, context); - @Override - public Class value() { - return CdiComponentProvider.class; - } - } + // supply registry + context.supply(GlobalServiceRegistry.CONTEXT_QUALIFIER, ServiceRegistry.class, + () -> ServiceRegistryManager.create().registry()); - @SuppressWarnings("ClassExplicitlyAnnotation") - private static final class SingletonLiteral extends AnnotationLiteral implements Singleton { - static final SingletonLiteral INSTANCE = new SingletonLiteral(); + return context; } - } diff --git a/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/HelidonTestNgListenerBase.java b/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/HelidonTestNgListenerBase.java new file mode 100644 index 00000000000..039d3044c8c --- /dev/null +++ b/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/HelidonTestNgListenerBase.java @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2025 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.testing.testng; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Semaphore; + +import io.helidon.microprofile.testing.HelidonTestInfo; +import io.helidon.microprofile.testing.HelidonTestInfo.ClassInfo; +import io.helidon.microprofile.testing.HelidonTestInfo.MethodInfo; + +import org.testng.IClassListener; +import org.testng.IConfigurationListener; +import org.testng.IInvokedMethod; +import org.testng.IInvokedMethodListener; +import org.testng.ITestClass; +import org.testng.ITestNGMethod; +import org.testng.ITestResult; +import org.testng.xml.XmlTest; + +/** + * Base listener. + * Implements the following features: + *

    + *
  • Single instrumented test class running at a time
  • + *
  • {@link #onAfterClass(ClassInfo)} invoked last
  • + *
+ */ +abstract class HelidonTestNgListenerBase implements IInvokedMethodListener, + IConfigurationListener, + IClassListener { + + private static final Map> CONTEXTS = new ConcurrentHashMap<>(); + private static final Semaphore SEMAPHORE = new Semaphore(1); + + /** + * Filter the given class. + * + * @param cls class + * @return {@code true} if should be processed, {@code false} otherwise + */ + abstract boolean filterClass(Class cls); + + /** + * Before invocation. + * + * @param classContext class context + * @param methodInfo invoked method info + * @param testInfo test info + */ + abstract void onBeforeInvocation(ClassContext classContext, MethodInfo methodInfo, HelidonTestInfo testInfo); + + /** + * After invocation. + * + * @param methodInfo invoked method info + * @param testInfo test info + * @param last {@code true} if this is the last invocation of {@code testInfo}, {@code false} otherwise + */ + abstract void onAfterInvocation(MethodInfo methodInfo, HelidonTestInfo testInfo, boolean last); + + /** + * Before class. + * + * @param classInfo class info + */ + abstract void onBeforeClass(ClassInfo classInfo); + + /** + * After class. + * + * @param classInfo class info + */ + abstract void onAfterClass(ClassInfo classInfo); + + @Override + public void onConfigurationFailure(ITestResult tr, ITestNGMethod tm) { + if (filterClass(realClass(tr))) { + ITestNGMethod im = tr.getMethod(); + classContext(im.getTestClass()).afterInvocation(im, tm); + } + } + + @Override + public void onConfigurationSuccess(ITestResult tr, ITestNGMethod tm) { + if (filterClass(realClass(tr))) { + ITestNGMethod im = tr.getMethod(); + classContext(im.getTestClass()).afterInvocation(im, tm); + } + } + + @Override + public void beforeConfiguration(ITestResult tr, ITestNGMethod tm) { + if (filterClass(realClass(tr))) { + ITestNGMethod im = tr.getMethod(); + classContext(im.getTestClass()).beforeInvocation(im, tm); + } + } + + @Override + public void beforeInvocation(IInvokedMethod im, ITestResult tr) { + if (filterClass(realClass(tr)) && im.isTestMethod()) { + ITestNGMethod tm = im.getTestMethod(); + classContext(tm.getTestClass()).beforeInvocation(tm, tm); + } + } + + @Override + public void afterInvocation(IInvokedMethod im, ITestResult tr) { + if (filterClass(realClass(tr)) && im.isTestMethod()) { + ITestNGMethod tm = im.getTestMethod(); + classContext(tm.getTestClass()).afterInvocation(tm, tm); + } + } + + @Override + public void onBeforeClass(ITestClass tc) { + Class cls = tc.getRealClass(); + if (filterClass(cls)) { + classContext(tc).beforeClass(); + } + } + + @Override + public void onAfterClass(ITestClass tc) { + if (filterClass(tc.getRealClass())) { + classContext(tc).afterClass(); + } + } + + private ClassContext classContext(ITestClass tc) { + return CONTEXTS.computeIfAbsent(tc.getXmlTest(), k -> new ConcurrentHashMap<>()) + .computeIfAbsent(tc, k -> new ClassContext(k, SEMAPHORE, this)); + } + + private static Class realClass(ITestResult tr) { + return tr.getTestClass().getRealClass(); + } +} diff --git a/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/HelidonTestNgModuleFactory.java b/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/HelidonTestNgModuleFactory.java new file mode 100644 index 00000000000..f8fddcf0142 --- /dev/null +++ b/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/HelidonTestNgModuleFactory.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2025 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.testing.testng; + +import io.helidon.microprofile.testing.Instrumented; + +import com.google.inject.AbstractModule; +import com.google.inject.Module; +import org.testng.IModuleFactory; +import org.testng.ITestContext; + +/** + * A simple Guice module factory that instantiates the instrumented test class. + */ +public class HelidonTestNgModuleFactory implements IModuleFactory { + @Override + public Module createModule(ITestContext context, Class testClass) { + return new ModuleImpl<>(testClass); + } + + private static class ModuleImpl extends AbstractModule { + private final Class testClass; + private final T testInstance; + + ModuleImpl(Class testClass) { + this.testClass = testClass; + this.testInstance = Instrumented.allocateInstance(testClass); + } + + @Override + protected void configure() { + bind(testClass).toProvider(() -> testInstance); + } + } +} diff --git a/microprofile/testing/testng/src/main/java/module-info.java b/microprofile/testing/testng/src/main/java/module-info.java index 02ddeb59bf3..c3850227199 100644 --- a/microprofile/testing/testng/src/main/java/module-info.java +++ b/microprofile/testing/testng/src/main/java/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2024 Oracle and/or its affiliates. + * Copyright (c) 2022, 2025 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. @@ -19,22 +19,13 @@ */ module io.helidon.microprofile.testing.testng { - requires io.helidon.config.mp; - requires io.helidon.config.yaml.mp; - requires io.helidon.common.testing.vitualthreads; - requires io.helidon.microprofile.cdi; - requires jakarta.cdi; - requires jakarta.inject; - requires jakarta.ws.rs; - requires microprofile.config.api; - requires org.testng; + requires io.helidon.microprofile.testing; - requires static io.helidon.microprofile.server; - requires static jersey.cdi1x; - requires static jersey.weld2.se; + requires org.testng; + requires com.google.guice; + requires io.helidon.service.registry; exports io.helidon.microprofile.testing.testng; provides org.testng.ITestNGListener with io.helidon.microprofile.testing.testng.HelidonTestNgListener; - -} \ No newline at end of file +} diff --git a/microprofile/tests/testing/junit5/pom.xml b/microprofile/tests/testing/junit5/pom.xml index 96223fb1873..b53c5519344 100644 --- a/microprofile/tests/testing/junit5/pom.xml +++ b/microprofile/tests/testing/junit5/pom.xml @@ -28,10 +28,9 @@ helidon-microprofile-tests-testing-junit5 Helidon Microprofile Tests Junit5 unit tests - - Test for JUnit5 integration to prevent cyclic dependendcies, - so the module can be used in MP config implementation - + + true + @@ -51,7 +50,7 @@ org.hamcrest - hamcrest-core + hamcrest-all test @@ -59,6 +58,11 @@ helidon-microprofile-testing-mocking test + + io.helidon.logging + helidon-logging-jul + test + org.mockito mockito-core @@ -69,6 +73,22 @@ junit-platform-testkit test - + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + + ${project.build.testOutputDirectory}/logging.properties + + + ${redirectTestOutputToFile} + + + + diff --git a/microprofile/tests/testing/junit5/src/test/java/io/helidon/microprofile/tests/testing/junit5/EnabledIfParameter.java b/microprofile/tests/testing/junit5/src/test/java/io/helidon/microprofile/tests/testing/junit5/EnabledIfParameter.java new file mode 100644 index 00000000000..39f9b3221d6 --- /dev/null +++ b/microprofile/tests/testing/junit5/src/test/java/io/helidon/microprofile/tests/testing/junit5/EnabledIfParameter.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2025 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.microprofile.tests.testing.junit5; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.jupiter.api.extension.ExtendWith; + +/** + * Enable tests based on parameters. + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@ExtendWith(EnabledIfParameterCondition.class) +@interface EnabledIfParameter { + /** + * Parameter key. + * + * @return key + */ + String key(); + + /** + * Parameter value. + * + * @return value + */ + String value(); +} diff --git a/microprofile/tests/testing/junit5/src/test/java/io/helidon/microprofile/tests/testing/junit5/EnabledIfParameterCondition.java b/microprofile/tests/testing/junit5/src/test/java/io/helidon/microprofile/tests/testing/junit5/EnabledIfParameterCondition.java new file mode 100644 index 00000000000..58a97ad050d --- /dev/null +++ b/microprofile/tests/testing/junit5/src/test/java/io/helidon/microprofile/tests/testing/junit5/EnabledIfParameterCondition.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2025 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.microprofile.tests.testing.junit5; + +import java.util.Optional; + +import org.junit.jupiter.api.extension.ConditionEvaluationResult; +import org.junit.jupiter.api.extension.ExecutionCondition; +import org.junit.jupiter.api.extension.ExtensionContext; + +import static org.junit.jupiter.api.extension.ConditionEvaluationResult.disabled; +import static org.junit.jupiter.api.extension.ConditionEvaluationResult.enabled; + +/** + * Condition implementation of {@link EnabledIfParameter}. + */ +final class EnabledIfParameterCondition implements ExecutionCondition { + + @Override + public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) { + return context.getElement() + .flatMap(e -> Optional.ofNullable(e.getAnnotation(EnabledIfParameter.class))) + .map(a -> context.getConfigurationParameter(a.key()) + .map(v -> v.equals(a.value()) ? + enabled("parameter match: %s".formatted(a.key())) : + disabled("parameter mismatch: %s!=%s".formatted(a.value(), v))) + .orElse(disabled("parameter not found: %s".formatted(a.key())))) + .orElse(enabled("@EnabledIfParameter not present")); + } +} diff --git a/microprofile/tests/testing/junit5/src/test/java/io/helidon/microprofile/tests/testing/junit5/TestAlternativeObserver.java b/microprofile/tests/testing/junit5/src/test/java/io/helidon/microprofile/tests/testing/junit5/TestAlternativeObserver.java new file mode 100644 index 00000000000..fcc48823da9 --- /dev/null +++ b/microprofile/tests/testing/junit5/src/test/java/io/helidon/microprofile/tests/testing/junit5/TestAlternativeObserver.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2025 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.tests.testing.junit5; + +import io.helidon.microprofile.testing.junit5.AddBean; +import io.helidon.microprofile.testing.junit5.HelidonTest; + +import jakarta.annotation.Priority; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.context.Initialized; +import jakarta.enterprise.event.Observes; +import jakarta.enterprise.inject.Alternative; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +@AddBean(TestAlternativeObserver.AlternativeObserver.class) +@HelidonTest +class TestAlternativeObserver { + + @Inject + Observer observer; + + @Test + void doTest() { + assertThat(observer.started, is(true)); + } + + @Singleton + static class Observer { + + boolean started = false; + + void onStart(@Observes @Initialized(ApplicationScoped.class) Object ignore) { + started = true; + } + } + + @Priority(0) + @Alternative + @Singleton + static class AlternativeObserver extends Observer { + } +} diff --git a/microprofile/tests/testing/junit5/src/test/java/io/helidon/microprofile/tests/testing/junit5/TestConcurrentExecution.java b/microprofile/tests/testing/junit5/src/test/java/io/helidon/microprofile/tests/testing/junit5/TestConcurrentExecution.java new file mode 100644 index 00000000000..bd0c6326a05 --- /dev/null +++ b/microprofile/tests/testing/junit5/src/test/java/io/helidon/microprofile/tests/testing/junit5/TestConcurrentExecution.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2025 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.microprofile.tests.testing.junit5; + +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; + +import io.helidon.microprofile.testing.junit5.AddBean; +import io.helidon.microprofile.testing.junit5.HelidonTest; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +@HelidonTest +@AddBean(TestConcurrentExecution.State.class) +class TestConcurrentExecution { + + @Inject + State state; + + @Test + @Execution(ExecutionMode.CONCURRENT) + void testA() throws InterruptedException { + state.add("a"); + state.countDownA(); + state.awaitB(); + assertThat(state.get(), is(Set.of("a", "b"))); + } + + @Test + @Execution(ExecutionMode.CONCURRENT) + void testB() throws InterruptedException { + state.awaitA(); + assertThat(state.get(), is(Set.of("a"))); + state.add("b"); + state.countDownB(); + } + + @ApplicationScoped + static class State { + + final Set events = ConcurrentHashMap.newKeySet(); + final CountDownLatch latchA = new CountDownLatch(1); + final CountDownLatch latchB = new CountDownLatch(1); + + void add(String event) { + events.add(event); + } + + Set get() { + return events; + } + + void countDownA() { + latchA.countDown(); + } + + void awaitA() throws InterruptedException { + latchA.await(); + } + + void countDownB() { + latchB.countDown(); + } + + void awaitB() throws InterruptedException { + latchB.await(); + } + } +} diff --git a/microprofile/tests/testing/junit5/src/test/java/io/helidon/microprofile/tests/testing/junit5/TestGlobalServiceRegistry.java b/microprofile/tests/testing/junit5/src/test/java/io/helidon/microprofile/tests/testing/junit5/TestGlobalServiceRegistry.java new file mode 100644 index 00000000000..c1d6a69d381 --- /dev/null +++ b/microprofile/tests/testing/junit5/src/test/java/io/helidon/microprofile/tests/testing/junit5/TestGlobalServiceRegistry.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2025 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.microprofile.tests.testing.junit5; + +import java.util.HashSet; +import java.util.Set; + +import io.helidon.common.context.Contexts; +import io.helidon.microprofile.testing.junit5.HelidonTest; +import io.helidon.service.registry.GlobalServiceRegistry; +import io.helidon.service.registry.ServiceRegistry; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; + +@HelidonTest +class TestGlobalServiceRegistry { + + private static final Set INSTANCES = new HashSet<>(); + + record MyService() { + } + + static void invoke() { + INSTANCES.add(System.identityHashCode(GlobalServiceRegistry.registry())); + } + + static { + invoke(); + } + + TestGlobalServiceRegistry() { + invoke(); + } + + @BeforeAll + static void beforeAll() { + invoke(); + } + + @BeforeEach + void beforeEach() { + invoke(); + } + + @Test + void firstTest() { + invoke(); + assertThat(INSTANCES.size(), is(1)); + assertThat(INSTANCES.iterator().next(), + is(not(System.identityHashCode(Contexts.globalContext() + .get(GlobalServiceRegistry.CONTEXT_QUALIFIER, ServiceRegistry.class) + .orElse(null))))); + } + + @AfterEach + void afterEach() { + invoke(); + } + + @AfterAll + static void afterAll() { + try { + invoke(); + assertThat(INSTANCES.size(), is(1)); + } finally { + INSTANCES.clear(); + } + } +} diff --git a/microprofile/tests/testing/junit5/src/test/java/io/helidon/microprofile/tests/testing/junit5/TestImplicitReset.java b/microprofile/tests/testing/junit5/src/test/java/io/helidon/microprofile/tests/testing/junit5/TestImplicitReset.java new file mode 100644 index 00000000000..e0a1a425c4b --- /dev/null +++ b/microprofile/tests/testing/junit5/src/test/java/io/helidon/microprofile/tests/testing/junit5/TestImplicitReset.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2025 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.tests.testing.junit5; + +import java.util.HashSet; +import java.util.Set; + +import io.helidon.microprofile.testing.junit5.AddBean; +import io.helidon.microprofile.testing.junit5.HelidonTest; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.spi.CDI; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +@HelidonTest +class TestImplicitReset { + + private final static Set CONTAINERS = new HashSet<>(); + + @Test + void first() { + boolean empty = CONTAINERS.isEmpty(); + assertThat(CONTAINERS.add(System.identityHashCode(CDI.current())), is(empty)); + } + + @Test + @AddBean(DummyBean.class) + void secondTest() { + assertThat(CONTAINERS.add(System.identityHashCode(CDI.current())), is(true)); + } + + @Test + void thirdTest() { + boolean empty = CONTAINERS.isEmpty(); + assertThat(CONTAINERS.add(System.identityHashCode(CDI.current())), is(empty)); + } + + @Test + @AddBean(DummyBean.class) + void fourthTest() { + assertThat(CONTAINERS.add(System.identityHashCode(CDI.current())), is(true)); + } + + @Test + void fifthTest() { + boolean empty = CONTAINERS.isEmpty(); + assertThat(CONTAINERS.add(System.identityHashCode(CDI.current())), is(empty)); + } + + @AfterAll + static void afterClass() { + CONTAINERS.clear(); + } + + @ApplicationScoped + static class DummyBean { + } +} diff --git a/microprofile/tests/testing/junit5/src/test/java/io/helidon/microprofile/tests/testing/junit5/TestImplicitResetOrder.java b/microprofile/tests/testing/junit5/src/test/java/io/helidon/microprofile/tests/testing/junit5/TestImplicitResetOrder.java new file mode 100644 index 00000000000..6c539403520 --- /dev/null +++ b/microprofile/tests/testing/junit5/src/test/java/io/helidon/microprofile/tests/testing/junit5/TestImplicitResetOrder.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2025 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.tests.testing.junit5; + +import java.util.HashSet; +import java.util.Set; + +import io.helidon.microprofile.testing.junit5.AddBean; +import io.helidon.microprofile.testing.junit5.HelidonTest; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.spi.CDI; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +@HelidonTest +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class TestImplicitResetOrder { + + private final static Set CONTAINERS = new HashSet<>(); + private static int count = 0; + + @Test + @Order(1) + void first() { + assertThat(++count, is(1)); + assertThat(CONTAINERS.add(System.identityHashCode(CDI.current())), is(true)); + } + + @Test + @Order(2) + @AddBean(DummyBean.class) + void secondTest() { + assertThat(++count, is(2)); + assertThat(CONTAINERS.add(System.identityHashCode(CDI.current())), is(true)); + } + + @Test + @Order(3) + void thirdTest() { + assertThat(++count, is(3)); + assertThat(CONTAINERS.add(System.identityHashCode(CDI.current())), is(true)); + } + + @Test + @Order(4) + @AddBean(DummyBean.class) + void fourthTest() { + assertThat(++count, is(4)); + assertThat(CONTAINERS.add(System.identityHashCode(CDI.current())), is(true)); + } + + @Test + @Order(5) + void fifthTest() { + assertThat(++count, is(5)); + assertThat(CONTAINERS.add(System.identityHashCode(CDI.current())), is(true)); + } + + @AfterAll + static void afterClass() { + CONTAINERS.clear(); + count = 0; + } + + @ApplicationScoped + static class DummyBean { + } +} 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 index 5720506120f..e441778690e 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 Oracle and/or its affiliates. + * Copyright (c) 2024, 2025 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. @@ -16,14 +16,11 @@ 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 jakarta.inject.Inject; + import io.helidon.microprofile.testing.junit5.AddBean; import io.helidon.microprofile.testing.junit5.AddConfig; import io.helidon.microprofile.testing.junit5.AddConfigBlock; @@ -33,9 +30,10 @@ import org.eclipse.microprofile.config.inject.ConfigProperty; import org.junit.jupiter.api.Test; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + @TestMetaAnnotation.MetaAnnotation -// @HelidonTest is still mandatory in the test and it has no effect in the MetaAnnotation -@HelidonTest class TestMetaAnnotation { @Inject @@ -78,8 +76,9 @@ void testIt() { """) @AddConfig(key = "second-key", value = "test-custom-config-second-value") @Configuration(configSources = {"testConfigSources.properties", "testConfigSources.yaml"}) + @HelidonTest @Retention(RetentionPolicy.RUNTIME) - static @interface MetaAnnotation { + @interface MetaAnnotation { } static class MyBean { diff --git a/microprofile/tests/testing/junit5/src/test/java/io/helidon/microprofile/tests/testing/junit5/TestMockBeanAnswer.java b/microprofile/tests/testing/junit5/src/test/java/io/helidon/microprofile/tests/testing/junit5/TestMockBeanAnswer.java index 1a5874f323e..2994a13829c 100644 --- a/microprofile/tests/testing/junit5/src/test/java/io/helidon/microprofile/tests/testing/junit5/TestMockBeanAnswer.java +++ b/microprofile/tests/testing/junit5/src/test/java/io/helidon/microprofile/tests/testing/junit5/TestMockBeanAnswer.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 Oracle and/or its affiliates. + * Copyright (c) 2024, 2025 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. @@ -19,7 +19,6 @@ import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; -import jakarta.enterprise.inject.Produces; import jakarta.inject.Inject; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; @@ -31,7 +30,6 @@ import org.junit.jupiter.api.Test; import org.mockito.Answers; -import org.mockito.MockSettings; import org.mockito.Mockito; @HelidonTest diff --git a/microprofile/tests/testing/junit5/src/test/java/io/helidon/microprofile/tests/testing/junit5/TestPerClass.java b/microprofile/tests/testing/junit5/src/test/java/io/helidon/microprofile/tests/testing/junit5/TestPerClass.java new file mode 100644 index 00000000000..8e6604f9025 --- /dev/null +++ b/microprofile/tests/testing/junit5/src/test/java/io/helidon/microprofile/tests/testing/junit5/TestPerClass.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2025 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.tests.testing.junit5; + +import java.util.HashSet; +import java.util.Set; + +import io.helidon.microprofile.testing.junit5.HelidonTest; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@HelidonTest +class TestPerClass { + + private final Set instances = new HashSet<>(); + + @Test + void firstTest() { + instances.add(System.identityHashCode(this)); + assertThat(instances.size(), is(1)); + } + + @Test + void secondTest() { + instances.add(System.identityHashCode(this)); + assertThat(instances.size(), is(1)); + } +} diff --git a/microprofile/tests/testing/junit5/src/test/java/io/helidon/microprofile/tests/testing/junit5/TestPerClassOrder.java b/microprofile/tests/testing/junit5/src/test/java/io/helidon/microprofile/tests/testing/junit5/TestPerClassOrder.java new file mode 100644 index 00000000000..8b06c0e9f86 --- /dev/null +++ b/microprofile/tests/testing/junit5/src/test/java/io/helidon/microprofile/tests/testing/junit5/TestPerClassOrder.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2025 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.tests.testing.junit5; + +import io.helidon.microprofile.testing.junit5.HelidonTest; + +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestMethodOrder; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +@HelidonTest +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class TestPerClassOrder { + + private int count; + + @Test + @Order(1) + void firstTest() { + assertThat(++count, is(1)); + } + + @Test + @Order(2) + void secondTest() { + assertThat(++count, is(2)); + } + + @Test + @Order(3) + void thirdTest() { + assertThat(++count, is(3)); + } +} diff --git a/microprofile/tests/testing/junit5/src/test/java/io/helidon/microprofile/tests/testing/junit5/TestPerMethod.java b/microprofile/tests/testing/junit5/src/test/java/io/helidon/microprofile/tests/testing/junit5/TestPerMethod.java index 17dece476c0..c2112feb339 100644 --- a/microprofile/tests/testing/junit5/src/test/java/io/helidon/microprofile/tests/testing/junit5/TestPerMethod.java +++ b/microprofile/tests/testing/junit5/src/test/java/io/helidon/microprofile/tests/testing/junit5/TestPerMethod.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2023 Oracle and/or its affiliates. + * Copyright (c) 2025 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. @@ -17,96 +17,43 @@ package io.helidon.microprofile.tests.testing.junit5; import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; import java.util.Set; import io.helidon.microprofile.testing.junit5.AddBean; -import io.helidon.microprofile.testing.junit5.AddConfig; -import io.helidon.microprofile.testing.junit5.AddExtension; -import io.helidon.microprofile.testing.junit5.DisableDiscovery; import io.helidon.microprofile.testing.junit5.HelidonTest; import jakarta.enterprise.context.ApplicationScoped; -import jakarta.enterprise.context.Initialized; -import jakarta.enterprise.event.Observes; -import jakarta.enterprise.inject.se.SeContainer; -import jakarta.enterprise.inject.spi.CDI; -import jakarta.enterprise.inject.spi.Extension; import jakarta.inject.Inject; -import org.eclipse.microprofile.config.inject.ConfigProperty; -import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.fail; -@HelidonTest(resetPerTest = true) +@AddBean(TestPerMethod.Instances.class) +@TestInstance(TestInstance.Lifecycle.PER_METHOD) +@HelidonTest class TestPerMethod { - private final static List ALL_CONTAINERS = new LinkedList<>(); - @Test - void testWithParameter(SeContainer container) { - ALL_CONTAINERS.add(container); - } - - @Test - @DisableDiscovery - void testWithParameterNoDiscovery(SeContainer container) { - ALL_CONTAINERS.add(container); - } + @Inject + private Instances instances; @Test - @AddConfig(key = "key-1", value = "value-1") - @AddBean(MyBean.class) - void testWithAdditionalConfig() { - String configured = CDI.current() - .select(MyBean.class) - .get() - .configured(); - - assertThat(configured, is("value-1")); + void firstTest() { + assertThat(instances.add(this), is(true)); } @Test - @AddExtension(MyExtension.class) - void testCustomExtension() { - assertThat("Extension should have been called, as it observes application scope", MyExtension.called, is(true)); - } - - @AfterAll - static void validateContainerInstances() { - Set used = new HashSet<>(); - try { - for (SeContainer container : ALL_CONTAINERS) { - if (!used.add(System.identityHashCode(container))) { - fail("Container instance used twice: " + container); - } - } - } finally { - ALL_CONTAINERS.clear(); - } - } - - static class MyBean { - private final String configured; - - @Inject - MyBean(@ConfigProperty(name = "key-1") String configured) { - this.configured = configured; - } - - String configured() { - return configured; - } + void secondTest() { + assertThat(instances.add(this), is(true)); } - public static class MyExtension implements Extension { - private static volatile boolean called = false; + @ApplicationScoped + static class Instances { + final Set instances = new HashSet<>(); - void observer(@Observes @Initialized(ApplicationScoped.class) final Object event) { - called = true; + boolean add(Object instance) { + return instances.add(System.identityHashCode(instance)); } } } diff --git a/microprofile/tests/testing/junit5/src/test/java/io/helidon/microprofile/tests/testing/junit5/TestPerMethodOrder.java b/microprofile/tests/testing/junit5/src/test/java/io/helidon/microprofile/tests/testing/junit5/TestPerMethodOrder.java new file mode 100644 index 00000000000..47ef0fb23fa --- /dev/null +++ b/microprofile/tests/testing/junit5/src/test/java/io/helidon/microprofile/tests/testing/junit5/TestPerMethodOrder.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2025 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.tests.testing.junit5; + +import io.helidon.microprofile.testing.junit5.AddBean; +import io.helidon.microprofile.testing.junit5.HelidonTest; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestMethodOrder; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +@HelidonTest +@AddBean(TestPerMethodOrder.Counter.class) +@TestInstance(TestInstance.Lifecycle.PER_METHOD) +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class TestPerMethodOrder { + + @Inject + Counter counter; + + @Test + @Order(1) + void firstTest() { + assertThat(counter.incrementAndGet(), is(1)); + } + + @Test + @Order(2) + void secondTest() { + assertThat(counter.incrementAndGet(), is(2)); + } + + @Test + @Order(3) + void thirdTest() { + assertThat(counter.incrementAndGet(), is(3)); + } + + @ApplicationScoped + static class Counter { + + private int i; + + int incrementAndGet() { + return ++i; + } + } +} diff --git a/microprofile/tests/testing/junit5/src/test/java/io/helidon/microprofile/tests/testing/junit5/TestPinnedThread.java b/microprofile/tests/testing/junit5/src/test/java/io/helidon/microprofile/tests/testing/junit5/TestPinnedThread.java index 398bd0c4424..76ad4a8ae9a 100644 --- a/microprofile/tests/testing/junit5/src/test/java/io/helidon/microprofile/tests/testing/junit5/TestPinnedThread.java +++ b/microprofile/tests/testing/junit5/src/test/java/io/helidon/microprofile/tests/testing/junit5/TestPinnedThread.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 Oracle and/or its affiliates. + * Copyright (c) 2024, 2025 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. @@ -40,11 +40,13 @@ import static org.junit.platform.testkit.engine.TestExecutionResultConditions.instanceOf; import static org.junit.platform.testkit.engine.TestExecutionResultConditions.message; +@SuppressWarnings("ALL") class TestPinnedThread { @Test void engineTest() { Events events = EngineTestKit.engine("junit-jupiter") + .configurationParameter("TestPinnedThread", "true") .selectors( selectClass(PinningTestCase.class), selectClass(PinningExtraThreadTestCase.class), @@ -64,7 +66,7 @@ void engineTest() { ); } - private Condition failedWithPinningException(String expectedPinningMethodName) { + private Condition failedWithPinningException(String expectedPinningMethodName) { return finishedWithFailure( instanceOf(PinningAssertionError.class), message(m -> m.startsWith("Pinned virtual threads were detected")) @@ -73,7 +75,7 @@ private Condition failedWithPinningExce s -> s .anyMatch(e -> e.getMethodName() .equals(expectedPinningMethodName))), - "Method with pinning is missing from stack strace.") + "Method with pinning is missing from stack strace.") ); } @@ -81,6 +83,7 @@ private Condition displayClass(Class clazz) { return displayName(Arrays.stream(clazz.getName().split("\\.")).toList().getLast()); } + @EnabledIfParameter(key = "TestPinnedThread", value = "true") @HelidonTest(pinningDetection = true) @AddBean(PinningTestCase.TestResource.class) static class PinningTestCase { @@ -108,6 +111,7 @@ void pinningTest(WebTarget target) { } } + @EnabledIfParameter(key = "TestPinnedThread", value = "true") @HelidonTest(pinningDetection = true) static class PinningExtraThreadTestCase { @@ -125,6 +129,7 @@ void pinningTest() throws InterruptedException { } } + @EnabledIfParameter(key = "TestPinnedThread", value = "true") @HelidonTest(pinningDetection = false) static class PinningDisabledExtraThreadTestCase { @@ -142,6 +147,7 @@ void pinningTest() throws InterruptedException { } } + @EnabledIfParameter(key = "TestPinnedThread", value = "true") @HelidonTest(pinningDetection = true) static class NoPinningTestCase { @@ -168,6 +174,7 @@ void pinningTest(WebTarget target) { } } + @EnabledIfParameter(key = "TestPinnedThread", value = "true") @HelidonTest(pinningDetection = true) static class NoPinningExtraThreadTestCase { diff --git a/microprofile/tests/testing/junit5/src/test/java/io/helidon/microprofile/tests/testing/junit5/TestResetPerTest.java b/microprofile/tests/testing/junit5/src/test/java/io/helidon/microprofile/tests/testing/junit5/TestResetPerTest.java new file mode 100644 index 00000000000..37637d8313b --- /dev/null +++ b/microprofile/tests/testing/junit5/src/test/java/io/helidon/microprofile/tests/testing/junit5/TestResetPerTest.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2020, 2025 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.tests.testing.junit5; + +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +import io.helidon.microprofile.testing.junit5.AddBean; +import io.helidon.microprofile.testing.junit5.AddConfig; +import io.helidon.microprofile.testing.junit5.AddExtension; +import io.helidon.microprofile.testing.junit5.DisableDiscovery; +import io.helidon.microprofile.testing.junit5.HelidonTest; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.context.Initialized; +import jakarta.enterprise.event.Observes; +import jakarta.enterprise.inject.se.SeContainer; +import jakarta.enterprise.inject.spi.CDI; +import jakarta.enterprise.inject.spi.Extension; +import jakarta.inject.Inject; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.fail; + +@AddBean(TestResetPerTest.DummyBean.class) +@HelidonTest(resetPerTest = true) +class TestResetPerTest { + private final static List ALL_CONTAINERS = new LinkedList<>(); + + private final DummyBean dummyBean; + + @Inject + TestResetPerTest(DummyBean dummyBean) { + this.dummyBean = dummyBean; + } + + @Test + void testWithParameter(SeContainer container) { + ALL_CONTAINERS.add(container); + } + + @Test + @DisableDiscovery + void testWithParameterNoDiscovery(SeContainer container) { + ALL_CONTAINERS.add(container); + } + + @Test + @AddConfig(key = "key-1", value = "value-1") + @AddBean(MyBean.class) + void testWithAdditionalConfig() { + String configured = CDI.current() + .select(MyBean.class) + .get() + .configured(); + + assertThat(configured, is("value-1")); + } + + @Test + @AddExtension(MyExtension.class) + void testCustomExtension() { + assertThat(MyExtension.called, is(true)); + } + + @AfterAll + static void validateContainerInstances() { + Set used = new HashSet<>(); + try { + for (SeContainer container : ALL_CONTAINERS) { + if (!used.add(System.identityHashCode(container))) { + fail("Container instance used twice: " + container); + } + } + } finally { + ALL_CONTAINERS.clear(); + } + } + + @ApplicationScoped + static class DummyBean { + } + + @ApplicationScoped + static class MyBean { + private final String configured; + + @Inject + MyBean(@ConfigProperty(name = "key-1") String configured) { + this.configured = configured; + } + + String configured() { + return configured; + } + } + + public static class MyExtension implements Extension { + private static volatile boolean called = false; + + void observer(@Observes @Initialized(ApplicationScoped.class) Object event) { + called = true; + } + } +} diff --git a/microprofile/tests/testing/junit5/src/test/resources/junit-platform.properties b/microprofile/tests/testing/junit5/src/test/resources/junit-platform.properties new file mode 100644 index 00000000000..99794e3ba19 --- /dev/null +++ b/microprofile/tests/testing/junit5/src/test/resources/junit-platform.properties @@ -0,0 +1,16 @@ +# +# Copyright (c) 2025 Oracle and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +junit.jupiter.execution.parallel.enabled=true diff --git a/microprofile/tests/testing/junit5/src/test/resources/logging.properties b/microprofile/tests/testing/junit5/src/test/resources/logging.properties new file mode 100644 index 00000000000..7454cdab319 --- /dev/null +++ b/microprofile/tests/testing/junit5/src/test/resources/logging.properties @@ -0,0 +1,21 @@ +# +# Copyright (c) 2024, 2025 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. +# + +handlers=io.helidon.logging.jul.HelidonConsoleHandler +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %5$s%6$s%n + +.level=SEVERE +io.helidon.microprofile.testing.level=FINEST diff --git a/microprofile/tests/testing/testng/pom.xml b/microprofile/tests/testing/testng/pom.xml index 7e8f9022f5a..e491d7cd13a 100644 --- a/microprofile/tests/testing/testng/pom.xml +++ b/microprofile/tests/testing/testng/pom.xml @@ -15,8 +15,8 @@ limitations under the License. --> + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 io.helidon.microprofile.tests.testing @@ -27,9 +27,9 @@ helidon-microprofile-tests-testing-testng Helidon Microprofile Tests TestNG unit tests - - Test for TestNG integration to prevent cyclic dependencies, so the module can be used in MP config implementation - + + true + @@ -48,13 +48,13 @@ test - org.hamcrest - hamcrest-core + io.helidon.microprofile.testing + helidon-microprofile-testing-mocking test - io.helidon.microprofile.testing - helidon-microprofile-testing-mocking + io.helidon.logging + helidon-logging-jul test @@ -62,10 +62,19 @@ mockito-core test + + org.slf4j + slf4j-jdk14 + test + + + org.hamcrest + hamcrest-all + test + - org.apache.maven.plugins @@ -74,6 +83,12 @@ test-suite.xml + + + ${project.build.testOutputDirectory}/logging.properties + + + ${redirectTestOutputToFile} diff --git a/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/EnabledIfParameter.java b/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/EnabledIfParameter.java new file mode 100644 index 00000000000..9555baf1414 --- /dev/null +++ b/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/EnabledIfParameter.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2025 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 java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Enable tests based on parameters. + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@interface EnabledIfParameter { + /** + * Parameter key. + * + * @return key + */ + String key(); + + /** + * Parameter value. + * + * @return value + */ + String value(); +} diff --git a/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/EnabledIfParameterListener.java b/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/EnabledIfParameterListener.java new file mode 100644 index 00000000000..f57ba042a56 --- /dev/null +++ b/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/EnabledIfParameterListener.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2025 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 java.util.List; +import java.util.ListIterator; + +import org.testng.IAlterSuiteListener; +import org.testng.xml.XmlClass; +import org.testng.xml.XmlPackage; +import org.testng.xml.XmlSuite; +import org.testng.xml.XmlTest; + +/** + * An {@link IAlterSuiteListener} implementation that supports {@link EnabledIfParameter}. + */ +public final class EnabledIfParameterListener implements IAlterSuiteListener { + + @Override + public void alter(List suites) { + for (XmlSuite suite : suites) { + ListIterator testsIt = suite.getTests().listIterator(); + while (testsIt.hasNext()) { + XmlTest xmlTest = testsIt.next(); + ListIterator classesIt = xmlTest.getXmlClasses().listIterator(); + while (classesIt.hasNext()) { + XmlClass xmlClass = classesIt.next(); + if (isDisabled(xmlClass, xmlTest)) { + // remove test class + classesIt.remove(); + removeIfEmpty(classesIt, testsIt); + } + } + ListIterator packagesIt = xmlTest.getXmlPackages().listIterator(); + while (packagesIt.hasNext()) { + XmlPackage xmlPackage = packagesIt.next(); + ListIterator pkgClassesIt = xmlPackage.getXmlClasses().listIterator(); + while (pkgClassesIt.hasNext()) { + XmlClass xmlClass = pkgClassesIt.next(); + if (isDisabled(xmlClass, xmlTest)) { + // remove test class + pkgClassesIt.remove(); + removeIfEmpty(pkgClassesIt, packagesIt); + removeIfEmpty(pkgClassesIt, testsIt); + } + } + } + } + } + } + + private static boolean isDisabled(XmlClass xmlClass, XmlTest xmlTest) { + Class testClass = xmlClass.getSupportClass(); + EnabledIfParameter annotation = testClass.getAnnotation(EnabledIfParameter.class); + return annotation != null && !annotation.value().equals(xmlTest.getParameter(annotation.key())); + } + + private static void removeIfEmpty(ListIterator childIt, ListIterator parentIt) { + if (!childIt.hasPrevious() && !childIt.hasNext()) { + parentIt.remove(); + } + } +} diff --git a/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/ParallelizerListenerImpl.java b/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/ParallelizerListenerImpl.java new file mode 100644 index 00000000000..d151c26a060 --- /dev/null +++ b/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/ParallelizerListenerImpl.java @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2025 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 java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import io.helidon.microprofile.testing.testng.HelidonTest; + +import org.testng.IAlterSuiteListener; +import org.testng.IMethodInstance; +import org.testng.IMethodInterceptor; +import org.testng.ITestClass; +import org.testng.ITestContext; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; +import org.testng.xml.XmlClass; +import org.testng.xml.XmlPackage; +import org.testng.xml.XmlSuite; +import org.testng.xml.XmlTest; + +/** + * An {@link IAlterSuiteListener} implementation that customizes the parallel mode. + */ +public class ParallelizerListenerImpl implements IAlterSuiteListener, IMethodInterceptor { + + @Override + public void alter(List suites) { + // set the parallel mode to methods + // and set the thread count to the max method count + for (XmlSuite suite : suites) { + suite.setParallel(XmlSuite.ParallelMode.METHODS); + for (XmlTest test : suite.getTests()) { + int maxCount = suite.getThreadCount(); + for (XmlClass xmlClass : test.getXmlClasses()) { + int count = methodsCount(xmlClass); + if (count > maxCount) { + maxCount = count; + } + } + for (XmlPackage xmlPackage : test.getXmlPackages()) { + for (XmlClass xmlClass : xmlPackage.getXmlClasses()) { + int count = methodsCount(xmlClass); + if (count > maxCount) { + maxCount = count; + } + } + } + test.setThreadCount(maxCount); + } + } + } + + @Override + public List intercept(List methods, ITestContext context) { + // sort methods by class to prevent deadlocks + Map> methodsByClass = new LinkedHashMap<>(); + for (IMethodInstance e : methods) { + methodsByClass.computeIfAbsent(e.getMethod().getTestClass(), k -> new ArrayList<>()) + .add(e); + } + return methodsByClass.values().stream() + .flatMap(List::stream) + .toList(); + } + + private static int methodsCount(XmlClass xmlClass) { + int count = 0; + Class testClass = xmlClass.getSupportClass(); + if (isHelidonTest(testClass)) { + for (Method method : testClass.getDeclaredMethods()) { + if (method.isAnnotationPresent(Test.class) + || method.isAnnotationPresent(BeforeClass.class) + || method.isAnnotationPresent(AfterClass.class)) { + count++; + } + } + } + return count; + } + + private static boolean isHelidonTest(Class testClass) { + Deque> types = new ArrayDeque<>(); + types.push(testClass); + while (!types.isEmpty()) { + Class type = types.pop(); + if (type.getPackage().getName().startsWith("java.")) { + continue; + } + Deque> aTypes = new ArrayDeque<>(); + for (Annotation annotation : type.getAnnotations()) { + aTypes.push(annotation.annotationType()); + } + while (!aTypes.isEmpty()) { + Class aType = aTypes.pop(); + if (aType.equals(HelidonTest.class)) { + return true; + } + for (Annotation e : aType.getAnnotations()) { + Class eType = e.annotationType(); + if (!eType.getPackage().getName().startsWith("java.") + && !aTypes.contains(eType)) { + aTypes.push(eType); + } + } + } + if (type.getSuperclass() != null) { + types.push(type.getSuperclass()); + } + for (Class aClass : type.getInterfaces()) { + if (!types.contains(aClass)) { + types.push(aClass); + } + } + } + return false; + } +} diff --git a/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestAddConfigBlockProperties.java b/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestAddConfigBlockProperties.java index e7bbbf90373..532e345824c 100644 --- a/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestAddConfigBlockProperties.java +++ b/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestAddConfigBlockProperties.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 Oracle and/or its affiliates. + * Copyright (c) 2024, 2025 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. @@ -32,7 +32,7 @@ some.key1=some.value1 some.key2=some.value2 """) -class TestAddConfigBlockProperties { +public class TestAddConfigBlockProperties { @Inject @ConfigProperty(name = "some.key1") diff --git a/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestAddConfigBlockYaml.java b/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestAddConfigBlockYaml.java index a23bd9e529c..dc34e0c4695 100644 --- a/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestAddConfigBlockYaml.java +++ b/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestAddConfigBlockYaml.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 Oracle and/or its affiliates. + * Copyright (c) 2024, 2025 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. @@ -34,7 +34,7 @@ another2: key: "another2.value" """) -class TestAddConfigBlockYaml { +public class TestAddConfigBlockYaml { @Inject @ConfigProperty(name = "another1.key") diff --git a/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestAlternativeObserver.java b/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestAlternativeObserver.java new file mode 100644 index 00000000000..e0cfa0f30f9 --- /dev/null +++ b/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestAlternativeObserver.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2025 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 io.helidon.microprofile.testing.testng.AddBean; +import io.helidon.microprofile.testing.testng.HelidonTest; + +import jakarta.annotation.Priority; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.context.Initialized; +import jakarta.enterprise.event.Observes; +import jakarta.enterprise.inject.Alternative; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import org.testng.annotations.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +@AddBean(TestAlternativeObserver.AlternativeObserver.class) +@HelidonTest +public class TestAlternativeObserver { + + @Inject + Observer observer; + + @Test + void doTest() { + assertThat(observer.started, is(true)); + } + + @Singleton + static class Observer { + + boolean started = false; + + void onStart(@Observes @Initialized(ApplicationScoped.class) Object ignore) { + started = true; + } + } + + @Priority(0) + @Alternative + @Singleton + static class AlternativeObserver extends Observer { + } +} diff --git a/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestConstructorInjection.java b/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestConstructorInjection.java new file mode 100644 index 00000000000..ad8de46cb25 --- /dev/null +++ b/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestConstructorInjection.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2025 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 io.helidon.microprofile.testing.testng.AddBean; +import io.helidon.microprofile.testing.testng.DisableDiscovery; +import io.helidon.microprofile.testing.testng.HelidonTest; + +import jakarta.enterprise.inject.Produces; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import org.testng.annotations.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * Test when discovery is disabled. + */ +@HelidonTest +@DisableDiscovery +@AddBean(TestConstructorInjection.MyBean.class) +public class TestConstructorInjection { + private final int currentPort; + + @Inject + public TestConstructorInjection(@Named("port") int currentPort) { + this.currentPort = currentPort; + } + + @Test + void testIt() { + assertThat(currentPort, is(423)); + } + + public static class MyBean { + @Produces + @Named("port") + public int currentPort() { + return 423; + } + } +} diff --git a/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestDefaults.java b/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestDefaults.java index a3d84cfe1b8..b08cc375a06 100644 --- a/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestDefaults.java +++ b/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestDefaults.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2022, 2025 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. @@ -22,7 +22,7 @@ import jakarta.inject.Inject; import org.eclipse.microprofile.config.inject.ConfigProperty; import org.testng.annotations.BeforeClass; -import org.testng.annotations.BeforeTest; +import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; import static org.hamcrest.CoreMatchers.is; @@ -41,17 +41,17 @@ public class TestDefaults { @ConfigProperty(name = PROPERTY_NAME, defaultValue = DEFAULT_VALUE) private String shouldNotExist; - private static boolean beforeAllCalled; - private boolean beforeEachCalled; + private boolean beforeClassCalled; + private boolean beforeMethodCalled; @BeforeClass - static void initClass() { - beforeAllCalled = true; + void beforeClass() { + beforeClassCalled = true; } - @BeforeTest - void beforeEach() { - beforeEachCalled = true; + @BeforeMethod + void beforeMethod() { + beforeMethodCalled = true; } @Test @@ -62,8 +62,7 @@ void testIt() { @Test void testLifecycleMethodsCalled() { - // this is to validate we can still use the usual junit methods - assertThat("Before all should have been called", beforeAllCalled, is(true)); - assertThat("Before each should have been called", beforeEachCalled, is(true)); + assertThat(beforeClassCalled, is(true)); + assertThat(beforeMethodCalled, is(true)); } -} \ No newline at end of file +} diff --git a/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestGlobalServiceRegistry.java b/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestGlobalServiceRegistry.java new file mode 100644 index 00000000000..b348e887f15 --- /dev/null +++ b/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestGlobalServiceRegistry.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2025 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 java.util.HashSet; +import java.util.Set; + +import io.helidon.common.context.Contexts; +import io.helidon.microprofile.testing.testng.HelidonTest; +import io.helidon.service.registry.GlobalServiceRegistry; +import io.helidon.service.registry.ServiceRegistry; + +import org.testng.annotations.AfterClass; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; + +@HelidonTest +public class TestGlobalServiceRegistry { + + private static final Set INSTANCES = new HashSet<>(); + + record MyService() { + } + + static void invoke() { + INSTANCES.add(System.identityHashCode(GlobalServiceRegistry.registry())); + } + + static { + invoke(); + } + + TestGlobalServiceRegistry() { + invoke(); + } + + @BeforeClass + void beforeAll() { + invoke(); + } + + @BeforeMethod + void beforeEach() { + invoke(); + } + + @Test + void firstTest() { + invoke(); + assertThat(INSTANCES.size(), is(1)); + assertThat(INSTANCES.iterator().next(), + is(not(System.identityHashCode(Contexts.globalContext() + .get(GlobalServiceRegistry.CONTEXT_QUALIFIER, ServiceRegistry.class) + .orElse(null))))); + } + + @AfterMethod + void afterEach() { + invoke(); + } + + @AfterClass + void afterAll() { + try { + invoke(); + assertThat(INSTANCES.size(), is(1)); + } finally { + INSTANCES.clear(); + } + } +} diff --git a/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestImplicitReset.java b/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestImplicitReset.java new file mode 100644 index 00000000000..3d5619fe037 --- /dev/null +++ b/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestImplicitReset.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2025 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 org.testng.TestListenerAdapter; +import org.testng.annotations.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +public class TestImplicitReset { + + @Test + void testImplicitReset() { + TestListenerAdapter tla = new TestNGRunner() + .name("TestImplicitReset") + .parameter("TestImplicitReset", "true") + .testClasses(TestImplicitResetExtra.class) + .run(); + + assertThat(tla.getPassedTests().size(), is(5)); + } + + + @Test + void testImplicitResetOrder() { + TestListenerAdapter tla = new TestNGRunner() + .name("TestImplicitResetOrder") + .parameter("TestImplicitReset", "true") + .testClasses(TestImplicitResetOrderExtra.class) + .run(); + + assertThat(tla.getPassedTests().size(), is(5)); + } +} diff --git a/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestImplicitResetExtra.java b/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestImplicitResetExtra.java new file mode 100644 index 00000000000..96252666c03 --- /dev/null +++ b/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestImplicitResetExtra.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2025 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 java.util.HashSet; +import java.util.Set; + +import io.helidon.microprofile.testing.testng.AddBean; +import io.helidon.microprofile.testing.testng.HelidonTest; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.spi.CDI; +import org.testng.annotations.AfterClass; +import org.testng.annotations.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +/** + * Test executed programmatically from {@link TestImplicitReset}. + */ +@EnabledIfParameter(key = "TestImplicitReset", value = "true") +@HelidonTest +public class TestImplicitResetExtra { + + private final static Set CONTAINERS = new HashSet<>(); + + @Test + void first() { + boolean empty = CONTAINERS.isEmpty(); + assertThat(CONTAINERS.add(System.identityHashCode(CDI.current())), is(empty)); + } + + @Test + @AddBean(DummyBean.class) + void secondTest() { + assertThat(CONTAINERS.add(System.identityHashCode(CDI.current())), is(true)); + } + + @Test + void thirdTest() { + boolean empty = CONTAINERS.isEmpty(); + assertThat(CONTAINERS.add(System.identityHashCode(CDI.current())), is(empty)); + } + + @Test + @AddBean(DummyBean.class) + void fourthTest() { + assertThat(CONTAINERS.add(System.identityHashCode(CDI.current())), is(true)); + } + + @Test + void fifthTest() { + boolean empty = CONTAINERS.isEmpty(); + assertThat(CONTAINERS.add(System.identityHashCode(CDI.current())), is(empty)); + } + + @AfterClass + static void afterClass() { + CONTAINERS.clear(); + } + + @ApplicationScoped + static class DummyBean { + } +} diff --git a/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestImplicitResetOrderExtra.java b/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestImplicitResetOrderExtra.java new file mode 100644 index 00000000000..ecafcdab46b --- /dev/null +++ b/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestImplicitResetOrderExtra.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2025 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 java.util.HashSet; +import java.util.Set; + +import io.helidon.microprofile.testing.testng.AddBean; +import io.helidon.microprofile.testing.testng.HelidonTest; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.spi.CDI; +import org.testng.annotations.AfterClass; +import org.testng.annotations.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +/** + * Test executed programmatically from {@link TestImplicitReset}. + */ +@EnabledIfParameter(key = "TestImplicitReset", value = "true") +@HelidonTest +public class TestImplicitResetOrderExtra { + + private final static Set CONTAINERS = new HashSet<>(); + private static int count = 0; + + @Test(priority = 1) + void first() { + assertThat(++count, is(1)); + assertThat(CONTAINERS.add(System.identityHashCode(CDI.current())), is(true)); + } + + @Test(priority = 2) + @AddBean(DummyBean.class) + void secondTest() { + assertThat(++count, is(2)); + assertThat(CONTAINERS.add(System.identityHashCode(CDI.current())), is(true)); + } + + @Test(priority = 3) + void thirdTest() { + assertThat(++count, is(3)); + assertThat(CONTAINERS.add(System.identityHashCode(CDI.current())), is(true)); + } + + @Test(priority = 4) + @AddBean(DummyBean.class) + void fourthTest() { + assertThat(++count, is(4)); + assertThat(CONTAINERS.add(System.identityHashCode(CDI.current())), is(true)); + } + + @Test(priority = 5) + void fifthTest() { + assertThat(++count, is(5)); + assertThat(CONTAINERS.add(System.identityHashCode(CDI.current())), is(true)); + } + + @AfterClass + static void afterClass() { + CONTAINERS.clear(); + count = 0; + } + + @ApplicationScoped + static class DummyBean { + } +} 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 index 85b6d8c4d31..a13f5500eb1 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 Oracle and/or its affiliates. + * Copyright (c) 2024, 2025 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. @@ -34,9 +34,7 @@ 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 { +public class TestMetaAnnotation { @Inject MyBean bean; @@ -78,8 +76,9 @@ void testIt() { """) @AddConfig(key = "second-key", value = "test-custom-config-second-value") @Configuration(configSources = {"testConfigSources.properties", "testConfigSources.yaml"}) + @HelidonTest @Retention(RetentionPolicy.RUNTIME) - static @interface MetaAnnotation { + @interface MetaAnnotation { } static class MyBean { diff --git a/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestMethodOrder.java b/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestMethodOrder.java new file mode 100644 index 00000000000..5077e958f49 --- /dev/null +++ b/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestMethodOrder.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2025 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 org.testng.TestListenerAdapter; +import org.testng.annotations.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +public class TestMethodOrder { + + @Test + void testMethodOrder() { + TestListenerAdapter tla = new TestNGRunner() + .name("TestMethodOrder") + .parameter("TestMethodOrder", "true") + .testClasses(TestMethodOrderExtra.class) + .run(); + assertThat(tla.getPassedTests().size(), is(3)); + } +} diff --git a/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestMethodOrderExtra.java b/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestMethodOrderExtra.java new file mode 100644 index 00000000000..7684e693a59 --- /dev/null +++ b/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestMethodOrderExtra.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2025 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 java.util.ArrayList; +import java.util.List; + +import io.helidon.microprofile.testing.testng.HelidonTest; + +import org.testng.annotations.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +/** + * Test executed programmatically from {@link TestMethodOrder}. + */ +@EnabledIfParameter(key = "TestMethodOrder", value = "true") +@HelidonTest +public class TestMethodOrderExtra { + + private final List list = new ArrayList<>(); + + @Test(priority = 1) + void firstTest() { + list.add("firstTest"); + assertThat(list, is(List.of("firstTest"))); + } + + @Test(priority = 2) + void secondTest() { + list.add("secondTest"); + assertThat(list, is(List.of("firstTest", "secondTest"))); + } + + @Test(priority = 3) + void thirdTest() { + list.add("thirdTest"); + assertThat(list, is(List.of("firstTest", "secondTest", "thirdTest"))); + } +} diff --git a/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestMockBeanArgumentMatcher.java b/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestMockBeanArgumentMatcher.java index bac3c2484fd..f4d1c1be695 100644 --- a/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestMockBeanArgumentMatcher.java +++ b/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestMockBeanArgumentMatcher.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 Oracle and/or its affiliates. + * Copyright (c) 2024, 2025 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. @@ -36,7 +36,7 @@ @HelidonTest @AddBean(TestMockBeanArgumentMatcher.Resource.class) @AddBean(TestMockBeanArgumentMatcher.Service.class) -class TestMockBeanArgumentMatcher { +public class TestMockBeanArgumentMatcher { @MockBean private Service service; diff --git a/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestMockBeanField.java b/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestMockBeanField.java index 2f63646acce..0888383cd6b 100644 --- a/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestMockBeanField.java +++ b/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestMockBeanField.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 Oracle and/or its affiliates. + * Copyright (c) 2024, 2025 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. @@ -37,7 +37,7 @@ @HelidonTest @AddBean(TestMockBeanField.Resource.class) @AddBean(TestMockBeanField.Service.class) -class TestMockBeanField { +public class TestMockBeanField { @Inject @MockBean diff --git a/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestMockBeanParameter.java b/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestMockBeanParameter.java new file mode 100644 index 00000000000..b5d1fe05b81 --- /dev/null +++ b/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestMockBeanParameter.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2025 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 io.helidon.microprofile.testing.testng.AddBean; +import io.helidon.microprofile.testing.testng.HelidonTest; +import io.helidon.microprofile.testing.mocking.MockBean; + +import jakarta.enterprise.inject.Produces; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.client.WebTarget; +import org.mockito.Answers; +import org.mockito.MockSettings; +import org.mockito.Mockito; +import org.testng.annotations.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +@HelidonTest +@AddBean(TestMockBeanParameter.Resource.class) +@AddBean(TestMockBeanParameter.Service.class) +public class TestMockBeanParameter { + + @Inject + @MockBean + private Service service; + + @Inject + private WebTarget target; + + @Test + void injectionTest() { + String response = target.path("/test").request().get(String.class); + // Defaults to specified in @Produces + assertThat(response, is("")); + Mockito.when(service.test()).thenReturn("Mocked"); + response = target.path("/test").request().get(String.class); + assertThat(response, is("Mocked")); + } + + @Produces + MockSettings mockSettings() { + return Mockito.withSettings().defaultAnswer(Answers.RETURNS_DEFAULTS); + } + + @Path("/test") + public static class Resource { + + @Inject + private Service service; + + @GET + public String test() { + return service.test(); + } + } + + static class Service { + + String test() { + return "Not Mocked"; + } + + } +} diff --git a/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestNGRunner.java b/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestNGRunner.java new file mode 100644 index 00000000000..72b9effc857 --- /dev/null +++ b/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestNGRunner.java @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2025 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 java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.testng.IAlterSuiteListener; +import org.testng.ITestResult; +import org.testng.TestListenerAdapter; +import org.testng.TestNG; +import org.testng.xml.XmlSuite; + +/** + * {@link TestNG} helper. + */ +class TestNGRunner { + + private final TestNG testng; + private final TestListenerAdapter tla; + private final Map parameters = new HashMap<>(); + private boolean printErrors = true; + + /** + * Create a new instance. + */ + TestNGRunner() { + tla = new TestListenerAdapter(); + testng = new TestNG(false); + testng.addListener(new SuiteParameterAdapter(parameters)); + testng.addListener(tla); + testng.setListenersToSkipFromBeingWiredInViaServiceLoaders(ParallelizerListenerImpl.class.getName()); + testng.setVerbose(0); + } + + /** + * Set the default test name. + * + * @param name name + * @return this instance + */ + TestNGRunner name(String name) { + testng.setDefaultTestName(name); + return this; + } + + /** + * Print errors. + * + * @param printErrors {@code true} to print errors + * @return this instance + */ + TestNGRunner printErrors(boolean printErrors) { + this.printErrors = printErrors; + return this; + } + + /** + * Set the test classes. + * + * @param classes test classes + * @return this instance + */ + TestNGRunner testClasses(Class... classes) { + testng.setTestClasses(classes); + return this; + } + + /** + * Set a parameter. + * + * @param name parameter name + * @param value parameter value + * @return this instance + */ + TestNGRunner parameter(String name, String value) { + parameters.put(name, value); + return this; + } + + /** + * Set the parallel mode. + * + * @param parallelMode parallel mode + * @return this instance + */ + TestNGRunner parallel(XmlSuite.ParallelMode parallelMode) { + testng.setParallel(parallelMode); + return this; + } + + /** + * Run the tests. + * + * @return TestListenerAdapter + */ + TestListenerAdapter run() { + testng.run(); + if (printErrors) { + for (ITestResult failedTest : tla.getFailedTests()) { + failedTest.getThrowable().printStackTrace(System.out); + } + } + return tla; + } + + private record SuiteParameterAdapter(Map parameters) implements IAlterSuiteListener { + @Override + public void alter(List suites) { + for (XmlSuite suite : suites) { + suite.getParameters().putAll(parameters); + } + } + } +} diff --git a/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestNamedWebTarget.java b/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestNamedWebTarget.java new file mode 100644 index 00000000000..3266b549c00 --- /dev/null +++ b/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestNamedWebTarget.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2025 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 io.helidon.microprofile.testing.Socket; +import io.helidon.microprofile.testing.testng.AddBean; +import io.helidon.microprofile.testing.testng.AddConfig; +import io.helidon.microprofile.testing.testng.HelidonTest; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.client.WebTarget; +import org.testng.annotations.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; + +@HelidonTest +@AddBean(TestNamedWebTarget.ResourceClass.class) +@AddConfig(key = "server.sockets.0.port", value = "0") +@AddConfig(key = "server.sockets.0.name", value = "named") +public class TestNamedWebTarget { + + @Inject + private WebTarget target; + + @Inject + @Socket("named") + private WebTarget namedTarget; + + @Test + void testTargetsAreDifferent(){ + //Should be different + assertThat(target.getUri(), not(namedTarget.getUri())); + } + + @Test + void testRegularTarget() { + assertThat(target, notNullValue()); + String response = target.path("/test") + .request() + .get(String.class); + assertThat(response, is("Hello from ResourceClass")); + } + + @Test + void testNamedSocketTarget() { + assertThat(namedTarget, notNullValue()); + String response = namedTarget.path("/test/named") + .request() + .get(String.class); + assertThat(response, is("Hello from Named Resource")); + } + + @Path("/test") + public static class ResourceClass { + @GET + public String getIt() { + return "Hello from ResourceClass"; + } + + @GET + @Path("named") + public String getNamed() { + return "Hello from Named Resource"; + } + } +} diff --git a/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestParallelMethods.java b/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestParallelMethods.java new file mode 100644 index 00000000000..fdf492ed240 --- /dev/null +++ b/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestParallelMethods.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2025 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 org.testng.TestListenerAdapter; +import org.testng.annotations.Test; +import org.testng.xml.XmlSuite; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +public class TestParallelMethods { + + @Test + void testParallelMethods() { + TestListenerAdapter tla = new TestNGRunner() + .name("TestParallelMethods") + .parameter("TestParallelMethods", "true") + .testClasses(TestParallelMethodsExtra.class) + .parallel(XmlSuite.ParallelMode.METHODS) + .run(); + assertThat(tla.getPassedTests().size(), is(2)); + } +} diff --git a/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestParallelMethodsExtra.java b/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestParallelMethodsExtra.java new file mode 100644 index 00000000000..8d2b3a88319 --- /dev/null +++ b/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestParallelMethodsExtra.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2025 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 java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; + +import io.helidon.microprofile.testing.testng.AddBean; +import io.helidon.microprofile.testing.testng.HelidonTest; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import org.testng.annotations.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +@HelidonTest +@AddBean(TestParallelMethodsExtra.State.class) +@EnabledIfParameter(key = "TestParallelMethods", value = "true") +public class TestParallelMethodsExtra { + + @Inject + State state; + + @Test + void testA() throws InterruptedException { + state.add("a"); + state.countDownA(); + state.awaitB(); + assertThat(state.get(), is(Set.of("a", "b"))); + } + + @Test + void testB() throws InterruptedException { + state.awaitA(); + assertThat(state.get(), is(Set.of("a"))); + state.add("b"); + state.countDownB(); + } + + @ApplicationScoped + static class State { + + final Set events = ConcurrentHashMap.newKeySet(); + final CountDownLatch latchA = new CountDownLatch(1); + final CountDownLatch latchB = new CountDownLatch(1); + + void add(String event) { + events.add(event); + } + + Set get() { + return events; + } + + void countDownA() { + latchA.countDown(); + } + + void awaitA() throws InterruptedException { + latchA.await(); + } + + void countDownB() { + latchB.countDown(); + } + + void awaitB() throws InterruptedException { + latchB.await(); + } + } + +} diff --git a/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestPinnedThread.java b/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestPinnedThread.java index 8fdd6bda3bc..bdafafa2a26 100644 --- a/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestPinnedThread.java +++ b/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestPinnedThread.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 Oracle and/or its affiliates. + * Copyright (c) 2024, 2025 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. @@ -19,33 +19,42 @@ import java.util.Arrays; import io.helidon.common.testing.virtualthreads.PinningAssertionError; -import io.helidon.microprofile.tests.testing.testng.programmatic.PinningExtraThreadTest; import org.hamcrest.BaseMatcher; import org.hamcrest.Description; -import org.testng.Assert; +import org.testng.ITestResult; import org.testng.TestListenerAdapter; -import org.testng.TestNG; import org.testng.annotations.Test; -import static org.hamcrest.CoreMatchers.startsWith; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.Matchers.startsWith; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.core.IsCollectionContaining.hasItem; public class TestPinnedThread { - private static final String EXPECTED_PINNING_METHOD_NAME = "lambda$testPinningExtraThread$0"; + private static final String EXPECTED_PINNING_METHOD_NAME = "lambda$testPinned$0"; @Test - void testListener() { - TestNG testng = new TestNG(); - testng.setTestClasses(new Class[] {PinningExtraThreadTest.class}); - TestListenerAdapter tla = new TestListenerAdapter(); - testng.addListener(tla); - PinningAssertionError pinningAssertionError = Assert.expectThrows(PinningAssertionError.class, testng::run); - assertThat(pinningAssertionError.getMessage(), startsWith("Pinned virtual threads were detected:")); - assertThat("Method with pinning is missing from stack strace.", Arrays.asList(pinningAssertionError.getStackTrace()), - hasItem(new StackTraceElementMatcher(EXPECTED_PINNING_METHOD_NAME))); + void testPinnedThread() { + TestListenerAdapter tla = new TestNGRunner() + .name("TestPinnedThread") + .parameter("TestPinnedThread", "true") + .testClasses(TestPinnedThreadExtra.class) + .printErrors(false) + .run(); + Throwable ex = tla.getFailedTests().stream() + .findFirst() + .map(ITestResult::getThrowable) + .orElse(null); + assertThat(ex, is(not(nullValue()))); + assertThat(ex, is(instanceOf(PinningAssertionError.class))); + assertThat(ex.getMessage(), startsWith("Pinned virtual threads were detected:")); + assertThat("Method with pinning is missing from stack strace.", Arrays.asList(ex.getStackTrace()), + hasItem(new StackTraceElementMatcher(EXPECTED_PINNING_METHOD_NAME))); } private static class StackTraceElementMatcher extends BaseMatcher { @@ -70,7 +79,6 @@ public void describeMismatch(Object o, Description description) { @Override public void describeTo(Description description) { - } } } diff --git a/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/programmatic/PinningExtraThreadTest.java b/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestPinnedThreadExtra.java similarity index 75% rename from microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/programmatic/PinningExtraThreadTest.java rename to microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestPinnedThreadExtra.java index 924106a43ff..b013aea96da 100644 --- a/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/programmatic/PinningExtraThreadTest.java +++ b/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestPinnedThreadExtra.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 Oracle and/or its affiliates. + * Copyright (c) 2024, 2025 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. @@ -14,20 +14,21 @@ * limitations under the License. */ -package io.helidon.microprofile.tests.testing.testng.programmatic; +package io.helidon.microprofile.tests.testing.testng; import io.helidon.microprofile.testing.testng.HelidonTest; import org.testng.annotations.Test; /** - * Test executed programmatically from TestPinnedThread Test. + * Test executed programmatically from {@link TestPinnedThread}. */ +@EnabledIfParameter(key = "TestPinnedThread", value = "true") @HelidonTest(pinningDetection = true) -public class PinningExtraThreadTest { +public class TestPinnedThreadExtra { @Test - void testPinningExtraThread() throws InterruptedException { + void testPinned() throws InterruptedException { Thread.ofVirtual().start(() -> { synchronized (this) { try { diff --git a/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestPerMethod.java b/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestResetPerTest.java similarity index 75% rename from microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestPerMethod.java rename to microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestResetPerTest.java index cc19145b992..7be3a7951f5 100644 --- a/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestPerMethod.java +++ b/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestResetPerTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2022, 2025 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. @@ -24,12 +24,12 @@ import io.helidon.microprofile.testing.testng.AddBean; import io.helidon.microprofile.testing.testng.AddConfig; import io.helidon.microprofile.testing.testng.AddExtension; +import io.helidon.microprofile.testing.testng.DisableDiscovery; import io.helidon.microprofile.testing.testng.HelidonTest; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.Initialized; import jakarta.enterprise.event.Observes; -import jakarta.enterprise.inject.se.SeContainer; import jakarta.enterprise.inject.spi.CDI; import jakarta.enterprise.inject.spi.Extension; import jakarta.inject.Inject; @@ -41,14 +41,33 @@ import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; +@AddBean(TestResetPerTest.DummyBean.class) @HelidonTest(resetPerTest = true) -public class TestPerMethod { - private final static List ALL_CONTAINERS = new LinkedList<>(); +public class TestResetPerTest { + private final static List> ALL_CONTAINERS = new LinkedList<>(); + + private final DummyBean dummyBean; + + @Inject + TestResetPerTest(DummyBean dummyBean) { + this.dummyBean = dummyBean; + } + + @Test + void testWithParameter() { + ALL_CONTAINERS.add(CDI.current()); + } + + @Test + @DisableDiscovery + void testWithParameterNoDiscovery() { + ALL_CONTAINERS.add(CDI.current()); + } @Test @AddConfig(key = "key-1", value = "value-1") @AddBean(MyBean.class) - public void testWithAdditionalConfig() { + void testWithAdditionalConfig() { String configured = CDI.current() .select(MyBean.class) .get() @@ -59,15 +78,15 @@ public void testWithAdditionalConfig() { @Test @AddExtension(MyExtension.class) - public void testCustomExtension() { + void testCustomExtension() { assertThat("Extension should have been called, as it observes application scope", MyExtension.called, is(true)); } @AfterClass - static void validateContainerInstances() { + void validateContainerInstances() { Set used = new HashSet<>(); try { - for (SeContainer container : ALL_CONTAINERS) { + for (CDI container : ALL_CONTAINERS) { if (!used.add(System.identityHashCode(container))) { Assert.fail("Container instance used twice: " + container); } @@ -77,7 +96,12 @@ static void validateContainerInstances() { } } - public static class MyBean { + @ApplicationScoped + static class DummyBean { + } + + @ApplicationScoped + static class MyBean { private final String configured; @Inject diff --git a/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestWebTarget.java b/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestWebTarget.java new file mode 100644 index 00000000000..f9172683320 --- /dev/null +++ b/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestWebTarget.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2025 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 io.helidon.microprofile.server.JaxRsCdiExtension; +import io.helidon.microprofile.server.ServerCdiExtension; +import io.helidon.microprofile.testing.testng.AddBean; +import io.helidon.microprofile.testing.testng.AddExtension; +import io.helidon.microprofile.testing.testng.DisableDiscovery; +import io.helidon.microprofile.testing.testng.HelidonTest; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.client.WebTarget; +import org.glassfish.jersey.ext.cdi1x.internal.CdiComponentProvider; +import org.testng.annotations.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; + +@HelidonTest(resetPerTest = true) +@DisableDiscovery +@AddExtension(ServerCdiExtension.class) +@AddExtension(JaxRsCdiExtension.class) +@AddExtension(CdiComponentProvider.class) +@AddBean(TestWebTarget.ResourceClass.class) +public class TestWebTarget { + + @Inject + private WebTarget target; + + @Test + void testFirst() { + assertThat(target, notNullValue()); + String response = target.path("/test") + .request() + .get(String.class); + assertThat(response, is("Hello from ResourceClass")); + } + + @Test + void testSecond() { + assertThat(target, notNullValue()); + String response = target.path("/test") + .request() + .get(String.class); + assertThat(response, is("Hello from ResourceClass")); + } + + @Path("/test") + public static class ResourceClass { + @GET + public String getIt() { + return "Hello from ResourceClass"; + } + } +} diff --git a/microprofile/tests/testing/testng/src/test/resources/META-INF/services/org.testng.ITestNGListener b/microprofile/tests/testing/testng/src/test/resources/META-INF/services/org.testng.ITestNGListener new file mode 100644 index 00000000000..936dc33f8a0 --- /dev/null +++ b/microprofile/tests/testing/testng/src/test/resources/META-INF/services/org.testng.ITestNGListener @@ -0,0 +1,2 @@ +io.helidon.microprofile.tests.testing.testng.ParallelizerListenerImpl +io.helidon.microprofile.tests.testing.testng.EnabledIfParameterListener diff --git a/microprofile/tests/testing/testng/src/test/resources/logging.properties b/microprofile/tests/testing/testng/src/test/resources/logging.properties new file mode 100644 index 00000000000..54c3f39b0e1 --- /dev/null +++ b/microprofile/tests/testing/testng/src/test/resources/logging.properties @@ -0,0 +1,21 @@ +# +# Copyright (c) 2025 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. +# + +handlers=io.helidon.logging.jul.HelidonConsoleHandler +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s !thread! %5$s%6$s%n + +.level=SEVERE +io.helidon.microprofile.testing.level=FINEST diff --git a/microprofile/tests/testing/testng/test-suite.xml b/microprofile/tests/testing/testng/test-suite.xml index a60e4f2eed7..02a2e6b2974 100644 --- a/microprofile/tests/testing/testng/test-suite.xml +++ b/microprofile/tests/testing/testng/test-suite.xml @@ -1,7 +1,7 @@ - - + - \ No newline at end of file + diff --git a/service/registry/src/main/java/io/helidon/service/registry/GlobalServiceRegistry.java b/service/registry/src/main/java/io/helidon/service/registry/GlobalServiceRegistry.java index 3d1c1d02829..408568a4bce 100644 --- a/service/registry/src/main/java/io/helidon/service/registry/GlobalServiceRegistry.java +++ b/service/registry/src/main/java/io/helidon/service/registry/GlobalServiceRegistry.java @@ -47,7 +47,14 @@ public final class GlobalServiceRegistry { *

* In normal application runtime we use {@link io.helidon.common.context.Contexts#globalContext()}. */ - public static final String STATIC_CONTEXT_CLASSIFIER = "helidon-registry-static-context"; + public static final Object STATIC_CONTEXT_CLASSIFIER = new Object(); + + /** + * Classifier used to register the global service registry. + * + * @see #STATIC_CONTEXT_CLASSIFIER + */ + public static final Object CONTEXT_QUALIFIER = new Object(); private static final ReadWriteLock RW_LOCK = new ReentrantReadWriteLock(); @@ -130,7 +137,7 @@ public static ServiceRegistry registry(Supplier registrySupplie public static ServiceRegistry registry(ServiceRegistry newGlobalRegistry) { RW_LOCK.writeLock().lock(); try { - context().register(ContextQualifier.INSTANCE, newGlobalRegistry); + context().register(CONTEXT_QUALIFIER, newGlobalRegistry); } finally { RW_LOCK.writeLock().unlock(); } @@ -150,16 +157,9 @@ private static Context context() { private static Optional current() { RW_LOCK.readLock().lock(); try { - return context().get(ContextQualifier.INSTANCE, ServiceRegistry.class); + return context().get(CONTEXT_QUALIFIER, ServiceRegistry.class); } finally { RW_LOCK.readLock().unlock(); } } - - private static final class ContextQualifier { - private static final ContextQualifier INSTANCE = new ContextQualifier(); - - private ContextQualifier() { - } - } } diff --git a/testing/junit5/src/main/java/io/helidon/testing/junit5/TestJunitExtension.java b/testing/junit5/src/main/java/io/helidon/testing/junit5/TestJunitExtension.java index 51cfb6de57e..de23afaee0a 100644 --- a/testing/junit5/src/main/java/io/helidon/testing/junit5/TestJunitExtension.java +++ b/testing/junit5/src/main/java/io/helidon/testing/junit5/TestJunitExtension.java @@ -16,9 +16,10 @@ package io.helidon.testing.junit5; +import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Constructor; import java.lang.reflect.Method; -import java.util.List; +import java.util.Arrays; import java.util.Optional; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; @@ -30,7 +31,6 @@ import io.helidon.logging.common.LogConfig; import io.helidon.service.registry.GlobalServiceRegistry; import io.helidon.service.registry.ServiceRegistry; -import io.helidon.service.registry.ServiceRegistryConfig; import io.helidon.service.registry.ServiceRegistryManager; import io.helidon.testing.TestException; import io.helidon.testing.TestRegistry; @@ -40,6 +40,7 @@ import org.junit.jupiter.api.extension.DynamicTestInvocationContext; import org.junit.jupiter.api.extension.Extension; import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ExtensionContext.Store.CloseableResource; import org.junit.jupiter.api.extension.InvocationInterceptor; import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.api.extension.ParameterResolutionException; @@ -62,6 +63,8 @@ public class TestJunitExtension implements Extension, AfterAllCallback, ParameterResolver { + private static final ExtensionContext.Namespace NAMESPACE = ExtensionContext.Namespace.create(TestJunitExtension.class); + static { LogConfig.initClass(); } @@ -73,21 +76,10 @@ protected TestJunitExtension() { } @Override - public void beforeAll(ExtensionContext context) { - Class testClass = context.getRequiredTestClass(); - Context helidonContext = Context.builder() - .id("test-" + testClass.getName() + "-" + System.identityHashCode(testClass)) - .build(); - // self-register, so this context is used even if the current context is some child of it - helidonContext.register(GlobalServiceRegistry.STATIC_CONTEXT_CLASSIFIER, helidonContext); - - ExtensionContext.Store store = extensionStore(context); - store.put(Context.class, helidonContext); - - run(context, () -> { - LogConfig.configureRuntime(); - createRegistry(store, testClass); - }); + public void beforeAll(ExtensionContext ctx) { + var store = store(ctx, ctx.getRequiredTestClass()); + initStaticContext(store, ctx); + run(ctx, LogConfig::configureRuntime); } @Override @@ -96,141 +88,180 @@ public void afterAll(ExtensionContext context) { } @Override - public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) + public boolean supportsParameter(ParameterContext pc, ExtensionContext ctx) throws ParameterResolutionException { - Class paramType = parameterContext.getParameter().getType(); - if (!GenericType.create(parameterContext.getParameter().getParameterizedType()) - .isClass()) { - return false; - } - return registrySupportedType(extensionContext, paramType); + return supplyChecked(ctx, () -> { + var paramType = pc.getParameter().getType(); + var genericParamType = GenericType.create(pc.getParameter().getParameterizedType()); + if (!genericParamType.isClass()) { + return false; + } + return supportedType(GlobalServiceRegistry.registry(), paramType); + }); } @Override - public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) + public Object resolveParameter(ParameterContext pc, ExtensionContext ctx) throws ParameterResolutionException { - Class paramType = parameterContext.getParameter().getType(); - - if (registrySupportedType(extensionContext, paramType)) { - // at this point in time the registry must be ready - return registry(extensionContext) - .orElseThrow() - .get(paramType); - } - throw new ParameterResolutionException("Failed to resolve parameter of type " - + paramType.getName()); + return supplyChecked(ctx, () -> { + var paramType = pc.getParameter().getType(); + var registry = GlobalServiceRegistry.registry(); + if (supportedType(registry, paramType)) { + return registry.get(paramType); + } + throw new ParameterResolutionException("Failed to resolve parameter of type " + + paramType.getName()); + }); } @Override public T interceptTestClassConstructor(Invocation invocation, - ReflectiveInvocationContext> invocationContext, - ExtensionContext extensionContext) throws Throwable { - return invoke(extensionContext, invocation); + ReflectiveInvocationContext> ic, + ExtensionContext ctx) throws Throwable { + return invoke(ctx, invocation); } @Override public void interceptBeforeAllMethod(Invocation invocation, - ReflectiveInvocationContext invocationContext, - ExtensionContext extensionContext) throws Throwable { - invoke(extensionContext, invocation); + ReflectiveInvocationContext ic, + ExtensionContext ctx) throws Throwable { + invoke(ctx, invocation); } @Override public void interceptBeforeEachMethod(Invocation invocation, - ReflectiveInvocationContext invocationContext, - ExtensionContext extensionContext) throws Throwable { - invoke(extensionContext, invocation); + ReflectiveInvocationContext ic, + ExtensionContext ctx) throws Throwable { + invoke(ctx, invocation); } @Override public void interceptTestMethod(Invocation invocation, - ReflectiveInvocationContext invocationContext, - ExtensionContext extensionContext) throws Throwable { - invoke(extensionContext, invocation); + ReflectiveInvocationContext ic, + ExtensionContext ctx) throws Throwable { + invoke(ctx, invocation); } @Override public T interceptTestFactoryMethod(Invocation invocation, - ReflectiveInvocationContext invocationContext, - ExtensionContext extensionContext) throws Throwable { - return invoke(extensionContext, invocation); + ReflectiveInvocationContext ic, + ExtensionContext ctx) throws Throwable { + return invoke(ctx, invocation); } @Override public void interceptTestTemplateMethod(Invocation invocation, - ReflectiveInvocationContext invocationContext, - ExtensionContext extensionContext) throws Throwable { - invoke(extensionContext, invocation); + ReflectiveInvocationContext ic, + ExtensionContext ctx) throws Throwable { + invoke(ctx, invocation); } @Override public void interceptDynamicTest(Invocation invocation, - DynamicTestInvocationContext invocationContext, - ExtensionContext extensionContext) throws Throwable { - invoke(extensionContext, invocation); + DynamicTestInvocationContext ic, + ExtensionContext ctx) throws Throwable { + invoke(ctx, invocation); } @Override public void interceptAfterEachMethod(Invocation invocation, - ReflectiveInvocationContext invocationContext, - ExtensionContext extensionContext) throws Throwable { - invoke(extensionContext, invocation); + ReflectiveInvocationContext ic, + ExtensionContext ctx) throws Throwable { + invoke(ctx, invocation); } @Override public void interceptAfterAllMethod(Invocation invocation, - ReflectiveInvocationContext invocationContext, - ExtensionContext extensionContext) throws Throwable { - invoke(extensionContext, invocation); + ReflectiveInvocationContext ic, + ExtensionContext ctx) throws Throwable { + invoke(ctx, invocation); } /** - * Service registry associated with the provided extension contexts - * (uses {@link #extensionStore(org.junit.jupiter.api.extension.ExtensionContext)}). + * Initialize the static context to be used for all actions this extension invokes, and to store the global instances. + * This extension creates a unit test context by default for each test class. * - * @param extensionContext extension context - * @return service registry + * @param ctx JUnit extension context */ - protected Optional registry(ExtensionContext extensionContext) { - return Optional.ofNullable(extensionStore(extensionContext) - .get(ServiceRegistry.class, ServiceRegistry.class)); + protected void initStaticContext(ExtensionContext ctx) { + initStaticContext(store(ctx, ctx.getRequiredTestClass()), ctx); } /** - * Extension store used by this extension to store context, service registry etc. + * Initialize the static context to be used for all actions this extension invokes, and to store the global instances. + * This extension creates a unit test context by default for each test class. * - * @param ctx extension context - * @return extension store + * @param store JUnit extension store + * @param ctx JUnit extension context */ - protected ExtensionContext.Store extensionStore(ExtensionContext ctx) { - Class testClass = ctx.getRequiredTestClass(); - return ctx.getStore(ExtensionContext.Namespace.create(testClass)); + protected void initStaticContext(ExtensionContext.Store store, ExtensionContext ctx) { + store.getOrComputeIfAbsent(Context.class, c -> { + var testClass = ctx.getRequiredTestClass(); + var context = Context.builder() + .id("test-" + testClass.getName() + "-" + System.identityHashCode(testClass)) + .build(); + + // self-register, so this context is used even if the current context is some child of it + context.register(GlobalServiceRegistry.STATIC_CONTEXT_CLASSIFIER, context); + + // supply registry + context.supply(GlobalServiceRegistry.CONTEXT_QUALIFIER, ServiceRegistry.class, () -> { + var manager = ServiceRegistryManager.create(); + var registry = manager.registry(); + store.put(ServiceRegistryManager.class, (CloseableResource) manager::shutdown); + store.put(ServiceRegistry.class, registry); + return registry; + }); + return context; + }); } /** - * Context to be used for all actions this extension invokes, and to store the global instances. - * This extension creates a unit test context by default for each test class. + * Get an object from the given store. * - * @param ctx JUnit extension context - * @param context Helidon context to set + * @param store store + * @param key key + * @param type type + * @param object type + * @return optional */ - protected void context(ExtensionContext ctx, Context context) { - // self-register, so this context is used even if the current context is some child of it - context.register(GlobalServiceRegistry.STATIC_CONTEXT_CLASSIFIER, context); - extensionStore(ctx) - .put(Context.class, context); + protected static Optional storeLookup(ExtensionContext.Store store, Object key, Class type) { + return Optional.ofNullable(store.get(key, type)); } /** - * The current context (if already available) that the test actions will be executed in. + * The current "static" context (if already available) that the test actions will be executed in. * * @param ctx JUnit extension context * @return context used by this extension */ - protected Optional context(ExtensionContext ctx) { - return Optional.ofNullable(extensionStore(ctx).get(Context.class, Context.class)); + protected Optional staticContext(ExtensionContext ctx) { + return storeLookup(store(ctx, ctx.getRequiredTestClass()), Context.class, Context.class); + } + + /** + * Get a JUnit extension store. + * + * @param ctx JUnit extension context + * @param qualifiers qualifiers + * @return JUnit extension store + */ + protected static ExtensionContext.Store store(ExtensionContext ctx, AnnotatedElement... qualifiers) { + ExtensionContext.Namespace ns; + if (qualifiers.length > 0) { + ns = NAMESPACE.append(Arrays.stream(qualifiers) + .map(e -> switch (e) { + case Class c -> c.getName(); + case Method m -> m.getName(); + default -> throw new IllegalArgumentException("Unsupported element: " + e); + }) + .toArray()); + } else { + ns = NAMESPACE; + } + return ctx.getStore(ns); } /** @@ -243,11 +274,11 @@ protected Optional context(ExtensionContext ctx) { * @throws Throwable in case the call to callable threw an exception */ protected T supply(ExtensionContext ctx, Supplier supplier) throws Throwable { - return Contexts.runInContext(context(ctx).orElseThrow(), supplier::get); + return Contexts.runInContext(staticContext(ctx).orElseThrow(), supplier::get); } /** - * Call a supplier that can throw {@link java.lang.Throwable} within context. + * Call a supplier that can throw {@link Throwable} within context. * * @param ctx JUnit extension context * @param supplier supplier to invoke @@ -261,7 +292,7 @@ protected T supplyChecked(ExtensionContext ctx, Functions.CheckedSupplier supplier) throws E { AtomicReference thrown = new AtomicReference<>(); - T response = Contexts.runInContext(context(ctx).orElseThrow(), () -> { + T response = Contexts.runInContext(staticContext(ctx).orElseThrow(), () -> { try { return supplier.get(); } catch (Throwable e) { @@ -272,7 +303,7 @@ protected T supplyChecked(ExtensionContext ctx, if (thrown.get() == null) { return response; } - Throwable throwable = thrown.get(); + var throwable = thrown.get(); if (throwable instanceof RuntimeException rte) { throw rte; } @@ -289,7 +320,7 @@ protected T supplyChecked(ExtensionContext ctx, * @param runnable runnable to run */ protected void run(ExtensionContext ctx, Runnable runnable) { - Contexts.runInContext(context(ctx).orElseThrow(), runnable); + Contexts.runInContext(staticContext(ctx).orElseThrow(), runnable); } /** @@ -304,7 +335,7 @@ protected void run(ExtensionContext ctx, Runnable runnable) { protected void runChecked(ExtensionContext ctx, Functions.CheckedRunnable runnable) throws E { AtomicReference thrown = new AtomicReference<>(); - Contexts.runInContext(context(ctx).orElseThrow(), () -> { + Contexts.runInContext(staticContext(ctx).orElseThrow(), () -> { try { runnable.run(); } catch (Throwable e) { @@ -315,7 +346,7 @@ protected void runChecked(ExtensionContext ctx, Functions. if (thrown.get() == null) { return; } - Throwable throwable = thrown.get(); + var throwable = thrown.get(); if (throwable instanceof RuntimeException rte) { throw rte; } @@ -337,7 +368,7 @@ protected void runChecked(ExtensionContext ctx, Functions. protected T invoke(ExtensionContext ctx, Invocation invocation) throws Throwable { AtomicReference thrown = new AtomicReference<>(); - T response = Contexts.runInContext(context(ctx).orElseThrow(), () -> { + T response = Contexts.runInContext(staticContext(ctx).orElseThrow(), () -> { try { return invocation.proceed(); } catch (Throwable e) { @@ -353,46 +384,25 @@ protected T invoke(ExtensionContext ctx, Invocation invocation) throws Th private void afterShutdownMethods(Class requiredTestClass) { for (Method declaredMethod : requiredTestClass.getDeclaredMethods()) { - TestRegistry.AfterShutdown annotation = declaredMethod.getAnnotation(TestRegistry.AfterShutdown.class); + var annotation = declaredMethod.getAnnotation(TestRegistry.AfterShutdown.class); if (annotation != null) { try { declaredMethod.setAccessible(true); declaredMethod.invoke(null); } catch (Exception e) { throw new TestException("Failed to invoke @TestRegistry.AfterShutdown annotated method " - + declaredMethod.getName(), e); + + declaredMethod.getName(), e); } } } } - private void createRegistry(ExtensionContext.Store store, Class testClass) { - var registryConfig = ServiceRegistryConfig.builder(); - var manager = ServiceRegistryManager.create(registryConfig.build()); - var registry = manager.registry(); - GlobalServiceRegistry.registry(registry); - store.put(ServiceRegistry.class, registry); - store.put(ServiceRegistryManager.class, new ClosableRegistryManager(manager)); - } - - private boolean registrySupportedType(ExtensionContext ctx, Class paramType) { + private boolean supportedType(ServiceRegistry registry, Class paramType) { if (ServiceRegistry.class.isAssignableFrom(paramType)) { return true; } // we do not want to get the instance here (yet) - return !registry(ctx) - .map(it -> it.allServices(paramType)) - .map(List::isEmpty) - .orElse(true); - } - - private record ClosableRegistryManager(ServiceRegistryManager manager) - implements ExtensionContext.Store.CloseableResource { - - @Override - public void close() { - manager.shutdown(); - } + return !registry.allServices(paramType).isEmpty(); } } diff --git a/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/AbstractGraphQLEndpointIT.java b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/AbstractGraphQLEndpointIT.java index 5cfc36b38db..0629c42ac54 100644 --- a/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/AbstractGraphQLEndpointIT.java +++ b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/AbstractGraphQLEndpointIT.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2023 Oracle and/or its affiliates. + * Copyright (c) 2020, 2025 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. @@ -22,18 +22,13 @@ import java.util.logging.Level; import java.util.logging.Logger; -import io.helidon.microprofile.cdi.Main; -import io.helidon.microprofile.server.ServerCdiExtension; - -import jakarta.enterprise.inject.spi.CDI; -import jakarta.ws.rs.client.Client; import jakarta.ws.rs.client.ClientBuilder; import jakarta.ws.rs.client.WebTarget; import jakarta.ws.rs.core.Response; import org.glassfish.jersey.client.ClientProperties; import org.glassfish.jersey.logging.LoggingFeature; -import org.junit.jupiter.api.AfterAll; +import static io.helidon.graphql.server.GraphQlConstants.GRAPHQL_WEB_CONTEXT; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.MatcherAssert.assertThat; @@ -48,86 +43,72 @@ public abstract class AbstractGraphQLEndpointIT extends AbstractGraphQLTest { /** * Initial GraphiQL query from UI. */ - protected static final String QUERY_INTROSPECT = "query {\n" - + " __schema {\n" - + " types {\n" - + " name\n" - + " }\n" - + " }\n" - + "}"; + protected static final String QUERY_INTROSPECT = """ + query { + __schema { + types { + name + } + } + }"""; protected static final String QUERY = "query"; protected static final String VARIABLES = "variables"; protected static final String OPERATION = "operationName"; - protected static final String GRAPHQL = "graphql"; - protected static final String UI = "ui"; - - private static String graphQLUrl; - private static Client client; + private final WebTarget target; - public static Client getClient() { - return client; + @SuppressWarnings("resource") + public AbstractGraphQLEndpointIT(String uri) { + this.target = ClientBuilder.newBuilder() + .register(new LoggingFeature(LOGGER, Level.WARNING, LoggingFeature.Verbosity.PAYLOAD_ANY, 32768)) + .property(ClientProperties.FOLLOW_REDIRECTS, true) + .build() + .target(uri) + .path(GRAPHQL_WEB_CONTEXT); } /** - * Startup the test and create the Jandex index with the supplied {@link Class}es. + * Create the Jandex index with the supplied classes. * - * @param clazzes {@link Class}es to add to index + * @param clazzes classes to add to index */ - public static void _startupTest(Class... clazzes) throws IOException { - // setup the Jandex index with the required classes + public static void setupIndex(Class... clazzes) throws IOException { + // set up the Jandex index with the required classes System.clearProperty(JandexUtils.PROP_INDEX_FILE); String indexFileName = getTempIndexFile(); setupIndex(indexFileName, clazzes); System.setProperty(JandexUtils.PROP_INDEX_FILE, indexFileName); - - Main.main(new String[0]); - - ServerCdiExtension current = CDI.current().getBeanManager().getExtension(ServerCdiExtension.class); - - graphQLUrl= "http://127.0.0.1:" + current.port() + "/"; - - System.out.println("GraphQL URL: " + graphQLUrl); - - client = ClientBuilder.newBuilder() - .register(new LoggingFeature(LOGGER, Level.WARNING, LoggingFeature.Verbosity.PAYLOAD_ANY, 32768)) - .property(ClientProperties.FOLLOW_REDIRECTS, true) - .build(); - } - - @AfterAll - public static void teardownTest() { - Main.shutdown(); } /** - * Return a {@link WebTarget} for the graphQL end point. + * Get the {@link WebTarget}. * - * @return a {@link WebTarget} for the graphQL end point + * @return WebTarget */ - protected static WebTarget getGraphQLWebTarget() { - Client client = getClient(); - return client.target(graphQLUrl); + protected WebTarget target() { + return target; } /** - * Encode the { and }. - * @param param {@link String} to encode - * @return an encoded @link String} + * Encode the { and }. + * + * @param param string to encode + * @return an encoded string */ - protected String encode(String param) { + protected String encode(String param) { return param == null ? null : param.replaceAll("}", "%7D").replaceAll("\\{", "%7B"); } /** - * Generate a Json Map with a request to send to graphql + * Generate a JSON Map with a request to send to graphql. * * @param query the query to send * @param operation optional operation * @param variables optional variables - * @return a {@link java.util.Map} + * @return map */ + @SuppressWarnings("SameParameterValue") protected Map generateJsonRequest(String query, String operation, Map variables) { Map map = new HashMap<>(); map.put(QUERY, query); @@ -141,10 +122,10 @@ protected Map generateJsonRequest(String query, String operation * Return the response as Json. * * @param response {@link jakarta.ws.rs.core.Response} received from web server - * @return the response as Json + * @return JSON entity as a map */ protected Map getJsonResponse(Response response) { - String stringResponse = (response.readEntity(String.class)); + String stringResponse = response.readEntity(String.class); assertThat(stringResponse, is(notNullValue())); return JsonUtils.convertJSONtoMap(stringResponse); } diff --git a/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/GraphQLEndpointIT.java b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/GraphQLEndpointIT.java index 33539ac6d3c..026958e8b53 100644 --- a/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/GraphQLEndpointIT.java +++ b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/GraphQLEndpointIT.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2023 Oracle and/or its affiliates. + * Copyright (c) 2020, 2025 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. @@ -28,7 +28,9 @@ import io.helidon.microprofile.testing.junit5.AddExtension; import io.helidon.microprofile.testing.junit5.DisableDiscovery; import io.helidon.microprofile.testing.junit5.HelidonTest; +import io.helidon.microprofile.testing.junit5.Socket; +import jakarta.inject.Inject; import jakarta.ws.rs.client.Entity; import jakarta.ws.rs.client.WebTarget; import jakarta.ws.rs.core.MediaType; @@ -39,7 +41,6 @@ import org.junit.jupiter.api.Test; import static io.helidon.graphql.server.GraphQlConstants.GRAPHQL_SCHEMA_URI; -import static io.helidon.graphql.server.GraphQlConstants.GRAPHQL_WEB_CONTEXT; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; @@ -60,17 +61,21 @@ public class GraphQLEndpointIT @BeforeAll public static void setup() throws IOException { - _startupTest(Person.class); + setupIndex(Person.class); + } + + @Inject + public GraphQLEndpointIT(@Socket("@default") String uri) { + super(uri); } @Test public void basicEndpointTests() { // test /graphql endpoint - WebTarget webTarget = getGraphQLWebTarget().path(GRAPHQL); Map mapRequest = generateJsonRequest(QUERY_INTROSPECT, null, null); // test POST - Response response = webTarget.request(MediaType.APPLICATION_JSON_TYPE) + Response response = target().request(MediaType.APPLICATION_JSON_TYPE) .post(Entity.json(JsonUtils.convertMapToJson(mapRequest))); assertThat(response, is(notNullValue())); assertThat(response.getStatus(), is(Response.Status.OK.getStatusCode())); @@ -80,12 +85,11 @@ public void basicEndpointTests() { assertThat(graphQLResults.size(), CoreMatchers.is(1)); // test GET - webTarget = getGraphQLWebTarget().path(GRAPHQL) + response = target() .queryParam(QUERY, encode((String) mapRequest.get(QUERY))) .queryParam(OPERATION, encode((String) mapRequest.get(OPERATION))) - .queryParam(VARIABLES, encode((String) mapRequest.get(VARIABLES))); - - response = webTarget.request(MediaType.APPLICATION_JSON_TYPE).get(); + .queryParam(VARIABLES, encode((String) mapRequest.get(VARIABLES))) + .request(MediaType.APPLICATION_JSON_TYPE).get(); assertThat(response, is(notNullValue())); assertThat(response.getStatus(), is(Response.Status.OK.getStatusCode())); graphQLResults = getJsonResponse(response); @@ -95,7 +99,7 @@ public void basicEndpointTests() { @Test public void testGetSchema() { - WebTarget webTarget = getGraphQLWebTarget().path(GRAPHQL_WEB_CONTEXT).path(GRAPHQL_SCHEMA_URI); + WebTarget webTarget = target().path(GRAPHQL_SCHEMA_URI); Response response = webTarget.request(MediaType.TEXT_PLAIN).get(); assertThat(response, is(notNullValue())); assertThat(response.getStatus(), is(Response.Status.OK.getStatusCode())); diff --git a/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/IngorableIT.java b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/IgnorableIT.java similarity index 95% rename from tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/IngorableIT.java rename to tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/IgnorableIT.java index 7d0b26b3629..2e21d4dee79 100644 --- a/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/IngorableIT.java +++ b/tests/integration/mp-graphql/src/test/java/io/helidon/microprofile/graphql/server/IgnorableIT.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2023 Oracle and/or its affiliates. + * Copyright (c) 2020, 2025 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. @@ -38,10 +38,10 @@ */ @AddBean(QueriesWithIgnorable.class) @AddBean(TestDB.class) -class IngorableIT extends AbstractGraphQlCdiIT { +class IgnorableIT extends AbstractGraphQlCdiIT { @Inject - IngorableIT(GraphQlCdiExtension graphQlCdiExtension) { + IgnorableIT(GraphQlCdiExtension graphQlCdiExtension) { super(graphQlCdiExtension); } diff --git a/webserver/testing/junit5/junit5/src/main/java/io/helidon/webserver/testing/junit5/HelidonServerJunitExtension.java b/webserver/testing/junit5/junit5/src/main/java/io/helidon/webserver/testing/junit5/HelidonServerJunitExtension.java index be653e1d7e0..475ad525eec 100644 --- a/webserver/testing/junit5/junit5/src/main/java/io/helidon/webserver/testing/junit5/HelidonServerJunitExtension.java +++ b/webserver/testing/junit5/junit5/src/main/java/io/helidon/webserver/testing/junit5/HelidonServerJunitExtension.java @@ -110,7 +110,7 @@ public void beforeAll(ExtensionContext context) { addRouting(builder); server = builder - .serverContext(super.context(context).orElseThrow()) // created above when we call super.beforeAll + .serverContext(staticContext(context).orElseThrow()) // created above when we call super.beforeAll .build() .start(); if (server.hasTls()) {

- * To disable automated bean and extension discovery, annotate the class with - * {@link DisableDiscovery}. + * A shorthand to use {@link HelidonTestNgListener} with additional settings. + * + * @see HelidonTestNgListener */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Inherited public @interface HelidonTest { /** - * By default, CDI container is created once before the class is initialized and shut down - * after. All test methods run within the same container. - * - * If this is set to {@code true}, a container is created per test method invocation. - * This restricts the test in the following way: - * 1. No injection into fields - * 2. No injection into constructor + * Forces the CDI container to be initialized and shutdown for each test method. * - * @return whether to reset container per test method + * @return whether to reset per test method */ boolean resetPerTest() default false; diff --git a/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/HelidonTestDescriptorImpl.java b/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/HelidonTestDescriptorImpl.java new file mode 100644 index 00000000000..c066ce579ee --- /dev/null +++ b/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/HelidonTestDescriptorImpl.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2025 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.testing.testng; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.util.List; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Stream; + +import io.helidon.microprofile.testing.HelidonTestDescriptorBase; + +import static io.helidon.microprofile.testing.Proxies.mirror; + +/** + * Base descriptor implementation that supports the deprecated annotations. + */ +@SuppressWarnings("deprecation") +class HelidonTestDescriptorImpl extends HelidonTestDescriptorBase { + + HelidonTestDescriptorImpl(T element) { + super(element); + } + + @Override + protected List lookupAddBeans() { + return lookup(io.helidon.microprofile.testing.AddBean.class, super.lookupAddBeans().stream(), + AddBean.class, AddBeans.class, AddBeans::value).toList(); + } + + @Override + protected List lookupAddConfigs() { + return lookup(io.helidon.microprofile.testing.AddConfig.class, super.lookupAddConfigs().stream(), + AddConfig.class, AddConfigs.class, AddConfigs::value).toList(); + } + + @Override + protected List lookupAddConfigBlocks() { + return Stream.concat(super.lookupAddConfigBlocks().stream(), annotations(AddConfigBlock.class) + .map(a -> mirror(io.helidon.microprofile.testing.AddConfigBlock.class, a))) + .toList(); + } + + @Override + protected List lookupAddExtensions() { + return lookup(io.helidon.microprofile.testing.AddExtension.class, super.lookupAddExtensions().stream(), + AddExtension.class, AddExtensions.class, AddExtensions::value).toList(); + } + + @Override + protected Optional lookupConfiguration() { + return super.lookupConfiguration().or(() -> annotations(Configuration.class) + .map(a -> mirror(io.helidon.microprofile.testing.Configuration.class, a)) + .findFirst()); + } + + @Override + protected boolean lookupAddJaxRs() { + return super.lookupAddJaxRs() || annotations(AddJaxRs.class) + .findFirst() + .isPresent(); + } + + @Override + protected boolean lookupDisableDiscovery() { + return super.lookupDisableDiscovery() || annotations(DisableDiscovery.class) + .findFirst() + .map(DisableDiscovery::value) + .orElse(false); + } + + @Override + protected boolean lookupResetPerTest() { + return annotations(HelidonTest.class) + .findFirst() + .map(HelidonTest::resetPerTest) + .orElse(false); + } + + @Override + protected boolean lookupPinningDetection() { + return annotations(HelidonTest.class) + .findFirst() + .map(HelidonTest::pinningDetection) + .orElse(false); + } + + @Override + public long pinningThreshold() { + return annotations(HelidonTest.class) + .findFirst() + .map(HelidonTest::pinningThreshold) + .orElse(20L); + } + + private Stream lookup(Class tType, + Stream initial, + Class aType, + Class cType, + Function function) { + + return Stream.concat(initial, annotations(aType, cType, function) + .map(a -> mirror(tType, a))); + } +} diff --git a/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/HelidonTestExtensionImpl.java b/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/HelidonTestExtensionImpl.java new file mode 100644 index 00000000000..62c1c057c80 --- /dev/null +++ b/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/HelidonTestExtensionImpl.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2025 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.testing.testng; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import io.helidon.microprofile.testing.HelidonTestExtension; +import io.helidon.microprofile.testing.HelidonTestInfo; +import io.helidon.microprofile.testing.HelidonTestScope; + +import static io.helidon.microprofile.testing.Proxies.mirror; + +/** + * An implementation of {@link io.helidon.microprofile.testing.HelidonTestExtension} that supports the deprecated annotations. + */ +@SuppressWarnings("deprecation") +final class HelidonTestExtensionImpl extends HelidonTestExtension { + + private static final Set> TYPE_ANNOTATION_TYPES = Set.of( + AddConfig.class, + AddConfigs.class, + AddConfigBlock.class, + Configuration.class); + + private static final Set> METHOD_ANNOTATION_TYPES = Set.of( + AddConfig.class, + AddConfigs.class, + AddConfigBlock.class, + Configuration.class); + + private final Set> typeAnnotationTypes; + private final Set> methodAnnotationTypes; + + HelidonTestExtensionImpl(HelidonTestInfo testInfo, HelidonTestScope testScope) { + super(testInfo, testScope); + this.typeAnnotationTypes = concat(super.typeAnnotationTypes(), TYPE_ANNOTATION_TYPES); + this.methodAnnotationTypes = concat(super.methodAnnotationTypes(), METHOD_ANNOTATION_TYPES); + } + + @Override + protected Set> typeAnnotationTypes() { + return typeAnnotationTypes; + } + + @Override + protected Set> methodAnnotationTypes() { + return methodAnnotationTypes; + } + + @Override + protected void processTypeAnnotation(Annotation annotation) { + switch (annotation) { + case Configuration e -> processConfiguration(e); + case AddConfig e -> processAddConfig(e); + case AddConfigs e -> processAddConfig(e.value()); + case AddConfigBlock e -> processAddConfigBlock(e); + default -> super.processTypeAnnotation(annotation); + } + } + + @Override + protected void processTestMethodAnnotation(Annotation annotation, Method method) { + switch (annotation) { + case Configuration e -> processConfiguration(e); + case AddConfig e -> processAddConfig(e); + case AddConfigs e -> processAddConfig(e.value()); + case AddConfigBlock e -> processAddConfigBlock(e); + default -> super.processTestMethodAnnotation(annotation, method); + } + } + + private void processConfiguration(Configuration annotation) { + processConfiguration(mirror(io.helidon.microprofile.testing.Configuration.class, annotation)); + } + + private void processAddConfigBlock(AddConfigBlock annotation) { + processAddConfigBlock(mirror(io.helidon.microprofile.testing.AddConfigBlock.class, annotation)); + } + + private void processAddConfig(AddConfig... annotations) { + for (AddConfig annotation : annotations) { + processAddConfig(mirror(io.helidon.microprofile.testing.AddConfig.class, annotation)); + } + } + + private static Set> concat(Set> set1, + Set> set2) { + + return Stream.concat(set1.stream(), set2.stream()).collect(Collectors.toSet()); + } +} 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 284ba26d518..da9bf75bb79 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 @@ -16,664 +16,297 @@ package io.helidon.microprofile.testing.testng; -import java.io.IOException; -import java.io.StringReader; +import java.lang.System.Logger; +import java.lang.System.Logger.Level; import java.lang.annotation.Annotation; -import java.lang.reflect.AnnotatedElement; -import java.lang.reflect.Array; -import java.lang.reflect.Constructor; -import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; -import java.net.URL; -import java.time.Duration; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Enumeration; -import java.util.HashMap; +import java.util.Comparator; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.stream.Collectors; - -import io.helidon.common.testing.virtualthreads.PinningRecorder; -import io.helidon.config.mp.MpConfigSources; -import io.helidon.microprofile.server.JaxRsCdiExtension; -import io.helidon.microprofile.server.ServerCdiExtension; - -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.enterprise.context.Dependent; -import jakarta.enterprise.context.RequestScoped; -import jakarta.enterprise.context.spi.CreationalContext; -import jakarta.enterprise.event.Observes; -import jakarta.enterprise.inject.se.SeContainer; -import jakarta.enterprise.inject.se.SeContainerInitializer; -import jakarta.enterprise.inject.spi.AfterBeanDiscovery; -import jakarta.enterprise.inject.spi.BeforeBeanDiscovery; -import jakarta.enterprise.inject.spi.CDI; -import jakarta.enterprise.inject.spi.Extension; -import jakarta.enterprise.inject.spi.InjectionPoint; -import jakarta.enterprise.inject.spi.InjectionTarget; -import jakarta.enterprise.inject.spi.ProcessInjectionTarget; -import jakarta.enterprise.inject.spi.configurator.AnnotatedTypeConfigurator; -import jakarta.enterprise.util.AnnotationLiteral; -import jakarta.inject.Inject; -import jakarta.inject.Singleton; -import jakarta.ws.rs.client.ClientBuilder; -import org.eclipse.microprofile.config.Config; -import org.eclipse.microprofile.config.spi.ConfigBuilder; -import org.eclipse.microprofile.config.spi.ConfigProviderResolver; -import org.eclipse.microprofile.config.spi.ConfigSource; -import org.glassfish.jersey.ext.cdi1x.internal.CdiComponentProvider; -import org.testng.IClassListener; -import org.testng.ITestClass; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Semaphore; + +import io.helidon.common.Functions.UncheckedException; +import io.helidon.common.context.Context; +import io.helidon.common.context.Contexts; +import io.helidon.microprofile.testing.HelidonTestContainer; +import io.helidon.microprofile.testing.HelidonTestInfo; +import io.helidon.microprofile.testing.HelidonTestInfo.ClassInfo; +import io.helidon.microprofile.testing.HelidonTestInfo.MethodInfo; +import io.helidon.microprofile.testing.HelidonTestScope; +import io.helidon.microprofile.testing.Proxies; +import io.helidon.service.registry.GlobalServiceRegistry; +import io.helidon.service.registry.ServiceRegistry; +import io.helidon.service.registry.ServiceRegistryManager; + +import org.testng.IAlterSuiteListener; +import org.testng.IMethodInstance; +import org.testng.IMethodInterceptor; +import org.testng.ISuite; +import org.testng.ISuiteListener; +import org.testng.ITestContext; import org.testng.ITestListener; -import org.testng.ITestResult; -import org.testng.annotations.Test; +import org.testng.ITestNGMethod; +import org.testng.annotations.BeforeSuite; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Guice; +import org.testng.xml.XmlClass; +import org.testng.xml.XmlPackage; +import org.testng.xml.XmlSuite; +import org.testng.xml.XmlTest; + +import static io.helidon.microprofile.testing.Instrumented.instrument; +import static io.helidon.microprofile.testing.Instrumented.isInstrumented; +import static io.helidon.microprofile.testing.testng.ClassContext.classInfo; +import static java.util.stream.Collectors.joining; /** - * TestNG extension to support Helidon CDI container in tests. + * A TestNG listener that integrates CDI with TestNG to support Helidon MP. + *