diff --git a/all/pom.xml b/all/pom.xml index 0abd9ea96f9..22df838b385 100644 --- a/all/pom.xml +++ b/all/pom.xml @@ -443,6 +443,10 @@ io.helidon.common.processor helidon-common-processor-class-model + + io.helidon.testing + helidon-testing + io.helidon.common.testing helidon-common-testing-junit5 @@ -924,6 +928,10 @@ io.helidon.http.media helidon-http-media-multipart + + io.helidon.testing + helidon-testing-junit5 + io.helidon.webserver helidon-webserver diff --git a/bom/pom.xml b/bom/pom.xml index d6b6d76825a..f0e750769d0 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -1213,6 +1213,16 @@ helidon-http-media-multipart ${helidon.version} + + io.helidon.testing + helidon-testing + ${helidon.version} + + + io.helidon.testing + helidon-testing-junit5 + ${helidon.version} + io.helidon.webserver helidon-webserver diff --git a/common/config/pom.xml b/common/config/pom.xml index 2347a0bdb91..c347ea42e26 100644 --- a/common/config/pom.xml +++ b/common/config/pom.xml @@ -37,6 +37,10 @@ io.helidon.common helidon-common-mapper + + io.helidon.common + helidon-common-context + org.junit.jupiter junit-jupiter-api diff --git a/common/config/src/main/java/io/helidon/common/config/Config.java b/common/config/src/main/java/io/helidon/common/config/Config.java index 5089bcf0368..3773aa59d87 100644 --- a/common/config/src/main/java/io/helidon/common/config/Config.java +++ b/common/config/src/main/java/io/helidon/common/config/Config.java @@ -35,6 +35,17 @@ static Config empty() { return EmptyConfig.EMPTY; } + /** + * Create a new instance of configuration from the default configuration sources. + * In case there is no {@link io.helidon.common.config.spi.ConfigProvider} available, returns + * {@link #empty()}. + * + * @return a new configuration + */ + static Config create() { + return GlobalConfig.create(); + } + /** * Returns the fully-qualified key of the {@code Config} node. *

diff --git a/common/config/src/main/java/io/helidon/common/config/GlobalConfig.java b/common/config/src/main/java/io/helidon/common/config/GlobalConfig.java index fd661893a5c..3b861e5c81a 100644 --- a/common/config/src/main/java/io/helidon/common/config/GlobalConfig.java +++ b/common/config/src/main/java/io/helidon/common/config/GlobalConfig.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2022, 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. @@ -19,12 +19,12 @@ import java.util.List; import java.util.Objects; import java.util.ServiceLoader; -import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; import io.helidon.common.HelidonServiceLoader; import io.helidon.common.LazyValue; import io.helidon.common.config.spi.ConfigProvider; +import io.helidon.common.context.ContextSingleton; /** * Global configuration can be set by a user before any Helidon code is invoked, to override default discovery @@ -37,18 +37,9 @@ */ public final class GlobalConfig { private static final Config EMPTY = Config.empty(); - private static final LazyValue DEFAULT_CONFIG = LazyValue.create(() -> { - List providers = HelidonServiceLoader.create(ServiceLoader.load(ConfigProvider.class)) - .asList(); - // no implementations available, use empty configuration - if (providers.isEmpty()) { - return EMPTY; - } - // there is a valid provider, let's use its default configuration - return providers.get(0) - .create(); - }); - private static final AtomicReference CONFIG = new AtomicReference<>(); + private static final LazyValue DEFAULT_CONFIG = LazyValue.create(GlobalConfig::create); + private static final ContextSingleton CONTEXT_VALUE = ContextSingleton.create(GlobalConfig.class, + Config.class); private GlobalConfig() { } @@ -59,7 +50,7 @@ private GlobalConfig() { * @return {@code true} if there is a global configuration set already, {@code false} otherwise */ public static boolean configured() { - return CONFIG.get() != null; + return CONTEXT_VALUE.isPresent(); } /** @@ -70,7 +61,7 @@ public static boolean configured() { * @see #config(java.util.function.Supplier, boolean) */ public static Config config() { - return configured() ? CONFIG.get() : DEFAULT_CONFIG.get(); + return CONTEXT_VALUE.value().orElseGet(DEFAULT_CONFIG); } /** @@ -86,7 +77,7 @@ public static Config config(Supplier config) { /** * Set global configuration. * - * @param config configuration to use + * @param config configuration to use * @param overwrite whether to overwrite an existing configured value * @return current global config */ @@ -96,8 +87,22 @@ public static Config config(Supplier config, boolean overwrite) { if (overwrite || !configured()) { // there is a certain risk we may do this twice, if two components try to set global config in parallel. // as the result was already unclear (as order matters), we do not need to be 100% thread safe here - CONFIG.set(config.get()); + CONTEXT_VALUE.set(config.get()); + return config(); + } else { + return CONTEXT_VALUE.get(config); } - return CONFIG.get(); + } + + static Config create() { + List providers = HelidonServiceLoader.create(ServiceLoader.load(ConfigProvider.class)) + .asList(); + // no implementations available, use empty configuration + if (providers.isEmpty()) { + return EMPTY; + } + // there is a valid provider, let's use its default configuration + return providers.getFirst() + .create(); } } diff --git a/common/config/src/main/java/module-info.java b/common/config/src/main/java/module-info.java index 9e0842c930e..5d614099170 100644 --- a/common/config/src/main/java/module-info.java +++ b/common/config/src/main/java/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2022, 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. @@ -19,6 +19,7 @@ */ module io.helidon.common.config { + requires io.helidon.common.context; requires transitive io.helidon.common; requires transitive io.helidon.common.mapper; diff --git a/common/context/src/main/java/io/helidon/common/context/ContextSingleton.java b/common/context/src/main/java/io/helidon/common/context/ContextSingleton.java new file mode 100644 index 00000000000..d955ca47766 --- /dev/null +++ b/common/context/src/main/java/io/helidon/common/context/ContextSingleton.java @@ -0,0 +1,184 @@ +/* + * 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.common.context; + +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.function.Supplier; + +/** + * A context based "singleton" value holder that provides an indirection mechanism for static values. + * "Singleton" values are stored and resolved from a context classified with {@value #STATIC_CONTEXT_CLASSIFIER} + * in the {@link io.helidon.common.context.Contexts#context()} current context}, + * or the {@link io.helidon.common.context.Contexts#globalContext() global context if none is defined}. + * + * @param type of the value stored in context + */ +public final class ContextSingleton { + /** + * Classifier used to register a context that is to serve as the static context. + */ + public static final String STATIC_CONTEXT_CLASSIFIER = "helidon-singleton-context"; + + private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); + private final ContextSingletonClassifier classifier; + private final Supplier supplier; + + private ContextSingleton(ContextSingletonClassifier qualifier, Supplier supplier) { + this.classifier = qualifier; + this.supplier = supplier; + } + + /** + * Create a new context value. + * + * @param ownerClass type owning this context singleton + * @param clazz type of the value + * @param type of the value + * @return a new context value with nothing set. + */ + public static ContextSingleton create(Class ownerClass, Class clazz) { + Objects.requireNonNull(ownerClass); + Objects.requireNonNull(clazz); + + return new ContextSingleton<>(new ContextSingletonClassifier<>(ownerClass, clazz), null); + } + + /** + * Create a new context value with a supplier of instances for each context. + * + * @param ownerClass type owning this context singleton + * @param clazz type of the value + * @param value value supplier + * @param type of the value + * @return a new context value + */ + public static ContextSingleton create(Class ownerClass, Class clazz, Supplier value) { + Objects.requireNonNull(ownerClass); + Objects.requireNonNull(clazz); + Objects.requireNonNull(value); + + return new ContextSingleton<>(new ContextSingletonClassifier<>(ownerClass, clazz), value); + } + + /** + * Set the value in current context. + * + * @param value value to use + */ + public void set(T value) { + lock.writeLock().lock(); + try { + context().register(classifier, value); + } finally { + lock.writeLock().unlock(); + } + } + + /** + * Get the current value in this context, or call the provided supplier to get the value and register + * it, if not yet registered. + * + * @return current value (or the new one obtained from supplier) + * @throws NoSuchElementException in case there is no value, and no supplier + * @see #set(Object) + */ + public T get() throws NoSuchElementException { + return get(this.supplier); + } + + /** + * Get the current value in this context, or call the provided supplier to get the value and register + * it, if not yet registered. + * + * @param supplier supplier to call if the current value is not present + * @return current value (or the new one obtained from supplier) + * @throws NoSuchElementException in case there is no value, and the supplier returned {@code null} + * @see #set(Object) + */ + public T get(Supplier supplier) { + var current = current(); + if (current.isPresent()) { + return current.get(); + } + lock.writeLock().lock(); + try { + current = current(); + if (current.isPresent()) { + return current.get(); + } + if (supplier != null) { + var value = supplier.get(); + set(value); + return value; + } + throw new NoSuchElementException("There is no value available in the current context, " + + "and supplier was not provided when creating this instance for: " + + " " + classifier); + } finally { + lock.writeLock().unlock(); + } + } + + /** + * Current value as an {@link java.util.Optional}. + * + * @return current value, or empty if not configured + */ + public Optional value() { + return current(); + } + + /** + * Whether there is a value in current context. + * + * @return {@code true} if there is a value + */ + public boolean isPresent() { + return current().isPresent(); + } + + private static Context context() { + Context globalContext = Contexts.globalContext(); + + // this is the context we expect to get (and set global instances) + return Contexts.context() + .orElse(globalContext) + .get(STATIC_CONTEXT_CLASSIFIER, Context.class) + .orElse(globalContext); + } + + private Optional current() { + lock.readLock().lock(); + try { + return context().get(classifier, classifier.valueType()); + } finally { + lock.readLock().unlock(); + } + } + + @SuppressWarnings("rawtypes") + private record ContextSingletonClassifier(Class contextSingleton, + Class ownerClass, + Class valueType) { + private ContextSingletonClassifier(Class ownerClass, Class valueType) { + this(ContextSingleton.class, ownerClass, valueType); + } + } +} diff --git a/common/context/src/main/java/io/helidon/common/context/Contexts.java b/common/context/src/main/java/io/helidon/common/context/Contexts.java index bacd3a9c048..bfb2bdad911 100644 --- a/common/context/src/main/java/io/helidon/common/context/Contexts.java +++ b/common/context/src/main/java/io/helidon/common/context/Contexts.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2020 Oracle and/or its affiliates. + * Copyright (c) 2019, 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. diff --git a/common/context/src/main/java/io/helidon/common/context/ListContext.java b/common/context/src/main/java/io/helidon/common/context/ListContext.java index 4cd453ecb0e..a6f9635b194 100644 --- a/common/context/src/main/java/io/helidon/common/context/ListContext.java +++ b/common/context/src/main/java/io/helidon/common/context/ListContext.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2021 Oracle and/or its affiliates. + * Copyright (c) 2019, 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. @@ -108,6 +108,11 @@ public Optional get(Object classifier, Class type) { } } + @Override + public String toString() { + return contextId; + } + long nextChildId() { return contextCounter.getAndUpdate(operand -> (operand == Long.MAX_VALUE) ? 1 : (operand + 1)); } diff --git a/common/mapper/pom.xml b/common/mapper/pom.xml index ea9309098cd..620b43a6970 100644 --- a/common/mapper/pom.xml +++ b/common/mapper/pom.xml @@ -41,6 +41,10 @@ io.helidon.common helidon-common + + io.helidon.common + helidon-common-context + org.junit.jupiter junit-jupiter-api diff --git a/common/mapper/src/main/java/io/helidon/common/mapper/GlobalManager.java b/common/mapper/src/main/java/io/helidon/common/mapper/GlobalManager.java index 2aeae42d9e2..8a30bf003a0 100644 --- a/common/mapper/src/main/java/io/helidon/common/mapper/GlobalManager.java +++ b/common/mapper/src/main/java/io/helidon/common/mapper/GlobalManager.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 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. @@ -16,25 +16,25 @@ package io.helidon.common.mapper; -import java.util.concurrent.atomic.AtomicReference; - import io.helidon.common.LazyValue; +import io.helidon.common.context.ContextSingleton; final class GlobalManager { private static final LazyValue DEFAULT_MAPPER = LazyValue.create(() -> MapperManager.builder() .useBuiltIn(true) .build()); - private static final AtomicReference MANAGER = new AtomicReference<>(); + private static final ContextSingleton CONTEXT_VALUE = ContextSingleton.create(GlobalManager.class, + MapperManager.class, + DEFAULT_MAPPER); private GlobalManager() { } public static void mapperManager(MapperManager manager) { - MANAGER.set(manager); + CONTEXT_VALUE.set(manager); } static MapperManager mapperManager() { - MapperManager mapperManager = MANAGER.get(); - return mapperManager == null ? DEFAULT_MAPPER.get() : mapperManager; + return CONTEXT_VALUE.get(); } } diff --git a/common/mapper/src/main/java/module-info.java b/common/mapper/src/main/java/module-info.java index 23f255af38b..6f384920098 100644 --- a/common/mapper/src/main/java/module-info.java +++ b/common/mapper/src/main/java/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2023 Oracle and/or its affiliates. + * Copyright (c) 2019, 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. @@ -18,6 +18,7 @@ * Helidon Common Mapper. */ module io.helidon.common.mapper { + requires io.helidon.common.context; requires transitive io.helidon.common; exports io.helidon.common.mapper; diff --git a/config/config/src/main/java/io/helidon/config/BuilderImpl.java b/config/config/src/main/java/io/helidon/config/BuilderImpl.java index 28e60577289..14347a65736 100644 --- a/config/config/src/main/java/io/helidon/config/BuilderImpl.java +++ b/config/config/src/main/java/io/helidon/config/BuilderImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2023 Oracle and/or its affiliates. + * Copyright (c) 2017, 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. @@ -516,7 +516,6 @@ static ConfigMapperManager buildMappers(List prioriti private static void loadMapperServices(List providers) { HelidonServiceLoader.builder(ServiceLoader.load(ConfigMapperProvider.class)) - .addService(new EnumMapperProvider()) .build() .forEach(mapper -> providers.add(new HelidonMapperWrapper(mapper, Weights.find(mapper, 100)))); } diff --git a/config/config/src/main/java/io/helidon/config/ConfigProvider.java b/config/config/src/main/java/io/helidon/config/ConfigProvider.java index d6ed2e86620..d490f9ab0d7 100644 --- a/config/config/src/main/java/io/helidon/config/ConfigProvider.java +++ b/config/config/src/main/java/io/helidon/config/ConfigProvider.java @@ -54,7 +54,10 @@ class ConfigProvider implements Config { .disableFilterServices() .update(it -> configFilters.get() .forEach(it::addFilter)) - .disableMapperServices() + //.disableMapperServices() + // cannot do this for now, removed ConfigMapperProvider from service loaded services, config does it on its + // own + // ObjectConfigMapper is before EnumMapper, and both are before essential and built-in .update(it -> configMappers.get() .forEach(it::addMapper)) .build(); diff --git a/config/config/src/main/java/io/helidon/config/EnumMapperProvider.java b/config/config/src/main/java/io/helidon/config/EnumMapperProvider.java index 746ccd5338a..f93bc5ea638 100644 --- a/config/config/src/main/java/io/helidon/config/EnumMapperProvider.java +++ b/config/config/src/main/java/io/helidon/config/EnumMapperProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 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. @@ -40,24 +40,30 @@ * * These conversions are intended to maximize ease-of-use for authors of config sources so the values need not be * upper-cased nor punctuated with underscores rather than the more conventional (in config at least) hyphen. - *

*

* The only hardship this imposes is if a confusingly-designed enum has values which differ only in case and the * string in the config source does not exactly match one of the enum value names. In such cases * the mapper will be unable to choose which enum value matches an ambiguous string. A developer faced with this * problem can simply provide her own explicit config mapping for that enum, for instance as a function parameter to * {@code Config#as}. - *

- * */ @Weight(EnumMapperProvider.WEIGHT) -class EnumMapperProvider implements ConfigMapperProvider { +public class EnumMapperProvider implements ConfigMapperProvider { /** * Priority with which the enum mapper provider is added to the collection of providers (user- and Helidon-provided). */ static final double WEIGHT = Weighted.DEFAULT_WEIGHT; + /** + * Required constructor for {@link java.util.ServiceLoader}. + */ + public EnumMapperProvider() { + /* + This is now a "proper" service, to make this available also when using ServiceRegistry + */ + } + @Override public Map, Function> mappers() { return Map.of(); diff --git a/config/config/src/main/java/module-info.java b/config/config/src/main/java/module-info.java index 03790f9abcf..87e4ad08678 100644 --- a/config/config/src/main/java/module-info.java +++ b/config/config/src/main/java/module-info.java @@ -51,6 +51,8 @@ with io.helidon.config.PropertiesConfigParser; provides io.helidon.common.config.spi.ConfigProvider with io.helidon.config.HelidonConfigProvider; + provides io.helidon.config.spi.ConfigMapperProvider + with io.helidon.config.EnumMapperProvider; // needed when running with modules - to make private methods accessible opens io.helidon.config to weld.core.impl, io.helidon.microprofile.cdi; diff --git a/config/config/src/main/resources/META-INF/helidon/service.loader b/config/config/src/main/resources/META-INF/helidon/service.loader index d07172ed9b3..c951c168384 100644 --- a/config/config/src/main/resources/META-INF/helidon/service.loader +++ b/config/config/src/main/resources/META-INF/helidon/service.loader @@ -1,4 +1,6 @@ # List of service contracts we want to support either from service registry, or from service loader io.helidon.config.spi.ConfigParser io.helidon.config.spi.ConfigFilter -io.helidon.config.spi.ConfigMapperProvider +# This cannot be done for now, as ObjectConfigMapper ends up before built-ins when +# we disable mapper services +# io.helidon.config.spi.ConfigMapperProvider diff --git a/integrations/micrometer/micrometer/src/main/java/io/helidon/integrations/micrometer/MicrometerFeature.java b/integrations/micrometer/micrometer/src/main/java/io/helidon/integrations/micrometer/MicrometerFeature.java index 8fa0199e130..f98357283bd 100644 --- a/integrations/micrometer/micrometer/src/main/java/io/helidon/integrations/micrometer/MicrometerFeature.java +++ b/integrations/micrometer/micrometer/src/main/java/io/helidon/integrations/micrometer/MicrometerFeature.java @@ -19,7 +19,7 @@ import java.util.function.Supplier; import io.helidon.common.config.Config; -import io.helidon.common.context.Contexts; +import io.helidon.common.context.ContextSingleton; import io.helidon.config.metadata.Configured; import io.helidon.webserver.http.HttpRouting; import io.helidon.webserver.http.ServerRequest; @@ -46,9 +46,11 @@ */ @Deprecated(forRemoval = true, since = "4.1") public class MicrometerFeature extends HelidonFeatureSupport { - static final String DEFAULT_CONTEXT = "/micrometer"; - private static final String SERVICE_NAME = "Micrometer"; + + private static final ContextSingleton METER_REGISTRY = ContextSingleton.create(MicrometerFeature.class, + MeterRegistry.class); +private static final String SERVICE_NAME = "Micrometer"; private static final System.Logger LOGGER = System.getLogger(MicrometerFeature.class.getName()); private final MeterRegistryFactory meterRegistryFactory; @@ -105,7 +107,7 @@ protected void postSetup(HttpRouting.Builder defaultRouting, HttpRouting.Builder @Override public void beforeStart() { - Contexts.globalContext().register(registry()); + METER_REGISTRY.set(registry()); LOGGER.log(System.Logger.Level.WARNING, "Micrometer integration is deprecated and will be removed in a future major release."); } diff --git a/integrations/oci/metrics/cdi/src/test/java/io/helidon/integrations/oci/metrics/cdi/OciMetricsCdiExtensionTest.java b/integrations/oci/metrics/cdi/src/test/java/io/helidon/integrations/oci/metrics/cdi/OciMetricsCdiExtensionTest.java index 31cf285dd4f..52e99481716 100644 --- a/integrations/oci/metrics/cdi/src/test/java/io/helidon/integrations/oci/metrics/cdi/OciMetricsCdiExtensionTest.java +++ b/integrations/oci/metrics/cdi/src/test/java/io/helidon/integrations/oci/metrics/cdi/OciMetricsCdiExtensionTest.java @@ -27,6 +27,7 @@ import io.helidon.metrics.api.Meter; import io.helidon.metrics.api.MeterRegistry; import io.helidon.metrics.api.Metrics; +import io.helidon.metrics.api.MetricsFactory; import io.helidon.microprofile.config.ConfigCdiExtension; import io.helidon.microprofile.server.JaxRsCdiExtension; import io.helidon.microprofile.server.ServerCdiExtension; @@ -53,6 +54,7 @@ import jakarta.enterprise.inject.spi.configurator.BeanConfigurator; import org.glassfish.jersey.ext.cdi1x.internal.CdiComponentProvider; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import static org.hamcrest.MatcherAssert.assertThat; @@ -85,7 +87,6 @@ class OciMetricsCdiExtensionTest { private static CountDownLatch countDownLatch = new CountDownLatch(1); private static PostMetricDataDetails postMetricDataDetails; private static boolean activateOciMetricsSupportIsInvoked; - private static MeterRegistry registry = Metrics.globalRegistry(); @AfterEach void resetState() { @@ -112,6 +113,7 @@ void testDisableOciMetrics() throws InterruptedException { } private void validateOciMetricsSupport(boolean enabled) throws InterruptedException { + MeterRegistry registry = Metrics.globalRegistry(); Counter c1 = registry.getOrCreate(Counter.builder(Meter.Scope.BASE + METRIC_NAME_SUFFIX) .scope(Meter.Scope.BASE)); c1.increment(); @@ -150,6 +152,7 @@ private void validateOciMetricsSupport(boolean enabled) throws InterruptedExcept registry.remove(c1); registry.remove(c2); registry.remove(c3); + clear(); } interface MetricDataDetailsOCIParams { @@ -233,4 +236,25 @@ protected void activateOciMetricsSupport(Config rootConfig, Config ociMetricsCon super.activateOciMetricsSupport(rootConfig, ociMetricsConfig, builder); } } + + static void clear() { + MetricsFactory.closeAll(); + + // And clear out Micrometer's global registry explicitly to be extra sure. + io.micrometer.core.instrument.MeterRegistry mmGlobal = io.micrometer.core.instrument.Metrics.globalRegistry; + mmGlobal.clear(); + + int delayMS = 250; + int maxSecondsToWait = 5; + int iterationsRemaining = (maxSecondsToWait * 1000) / delayMS; + + while (iterationsRemaining > 0 && !mmGlobal.getMeters().isEmpty()) { + iterationsRemaining--; + try { + TimeUnit.MILLISECONDS.sleep(delayMS); + } catch (InterruptedException e) { + throw new RuntimeException("Error awaiting clear-out of meter registries to finish", e); + } + } + } } diff --git a/integrations/oci/metrics/metrics/src/main/java/io/helidon/integrations/oci/metrics/OciMetricsSupport.java b/integrations/oci/metrics/metrics/src/main/java/io/helidon/integrations/oci/metrics/OciMetricsSupport.java index c088efa59b8..de55780d275 100644 --- a/integrations/oci/metrics/metrics/src/main/java/io/helidon/integrations/oci/metrics/OciMetricsSupport.java +++ b/integrations/oci/metrics/metrics/src/main/java/io/helidon/integrations/oci/metrics/OciMetricsSupport.java @@ -26,6 +26,8 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import io.helidon.common.context.Context; +import io.helidon.common.context.Contexts; import io.helidon.config.Config; import io.helidon.config.metadata.Configured; import io.helidon.config.metadata.ConfiguredOption; @@ -175,8 +177,11 @@ static String baseMetricUnits(String metricUnits) { } private void startExecutor() { + Context ctx = Contexts.context().orElseGet(Contexts::globalContext); scheduledExecutorService = Executors.newScheduledThreadPool(1); - scheduledExecutorService.scheduleAtFixedRate(this::pushMetrics, initialDelay, delay, schedulingTimeUnit); + scheduledExecutorService.scheduleAtFixedRate(() -> { + Contexts.runInContext(ctx, this::pushMetrics); + }, initialDelay, delay, schedulingTimeUnit); } private void pushMetrics() { diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/MetricsFactoryManager.java b/metrics/api/src/main/java/io/helidon/metrics/api/MetricsFactoryManager.java index 982520d59d0..d1cbbdae504 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/MetricsFactoryManager.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/MetricsFactoryManager.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 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. @@ -17,15 +17,13 @@ import java.lang.System.Logger.Level; import java.util.Collection; -import java.util.Objects; import java.util.ServiceLoader; -import java.util.concurrent.Callable; -import java.util.concurrent.locks.ReentrantLock; import io.helidon.common.HelidonServiceLoader; import io.helidon.common.LazyValue; import io.helidon.common.config.Config; import io.helidon.common.config.GlobalConfig; +import io.helidon.common.context.ContextSingleton; import io.helidon.metrics.spi.MetersProvider; import io.helidon.metrics.spi.MetricsFactoryProvider; import io.helidon.metrics.spi.MetricsProgrammaticConfig; @@ -52,17 +50,19 @@ class MetricsFactoryManager { * for obtaining new {@link io.helidon.metrics.api.MetricsFactory} instances; this module contains a no-op implementation * as a last resort. */ - private static final LazyValue METRICS_FACTORY_PROVIDER = - io.helidon.common.LazyValue.create(() -> { - MetricsFactoryProvider result = HelidonServiceLoader.builder(ServiceLoader.load(MetricsFactoryProvider.class)) - .addService(NoOpMetricsFactoryProvider.create(), Double.MIN_VALUE) - .build() - .iterator() - .next(); - LOGGER.log(Level.DEBUG, "Loaded metrics factory provider: {0}", - result.getClass().getName()); - return result; - }); + private static final ContextSingleton METRICS_FACTORY_PROVIDER = + ContextSingleton.create(MetricsFactoryManager.class, + MetricsFactoryProvider.class, () -> { + MetricsFactoryProvider result = + HelidonServiceLoader.builder(ServiceLoader.load(MetricsFactoryProvider.class)) + .addService(NoOpMetricsFactoryProvider.create(), Double.MIN_VALUE) + .build() + .iterator() + .next(); + LOGGER.log(Level.DEBUG, "Loaded metrics factory provider: {0}", + result.getClass().getName()); + return result; + }); /** * Config overrides that can change the {@link io.helidon.metrics.api.MetricsConfig} that is read from config sources * if there are specific requirements in a given runtime (e.g., MP) for certain settings. For example, the tag name used @@ -73,23 +73,21 @@ class MetricsFactoryManager { HelidonServiceLoader .create(ServiceLoader.load(MetricsProgrammaticConfig.class)) .asList()); - private static final ReentrantLock LOCK = new ReentrantLock(); /** * Providers of meter builders (such as the built-in "base" meters for system performance information). All providers are * furnished to all {@link io.helidon.metrics.api.MeterRegistry} instances that are created by any * {@link io.helidon.metrics.api.MetricsFactory}. */ - private static final LazyValue> METER_PROVIDERS = - LazyValue.create(() -> HelidonServiceLoader.create(ServiceLoader.load(MetersProvider.class)) - .asList()); - /** - * The metrics {@link io.helidon.common.config.Config} node used to initialize the current metrics factory. - */ - private static Config metricsConfigNode; - /** - * The {@link io.helidon.metrics.api.MetricsFactory} most recently created via either {@link #getMetricsFactory} method. - */ - private static MetricsFactory metricsFactory; + private static final ContextSingleton METER_PROVIDERS = + ContextSingleton.create(MetricsFactoryManager.class, + MetersProviders.class, + () -> + new MetersProviders(HelidonServiceLoader.create(ServiceLoader.load(MetersProvider.class)).asList())); + // we cannot use Config directly, as that would conflict with GlobalConfig itself + private static final ContextSingleton METRICS_CONFIG = + ContextSingleton.create(MetricsFactoryManager.class, MetricsConfigHolder.class); + private static final ContextSingleton METRICS_FACTORY = ContextSingleton.create(MetricsFactoryManager.class, + MetricsFactory.class); private MetricsFactoryManager() { } @@ -103,12 +101,10 @@ private MetricsFactoryManager() { * @return new metrics factory */ static MetricsFactory getMetricsFactory(Config metricsConfigNode) { + METRICS_CONFIG.set(new MetricsConfigHolder(metricsConfigNode)); - MetricsFactoryManager.metricsConfigNode = metricsConfigNode; - - MetricsConfig metricsConfig = MetricsConfig.create(metricsConfigNode); - - metricsFactory = access(() -> completeGetInstance(metricsConfig, metricsConfigNode)); + MetricsFactory metricsFactory = buildMetricsFactory(metricsConfigNode); + METRICS_FACTORY.set(metricsFactory); return metricsFactory; } @@ -121,12 +117,11 @@ static MetricsFactory getMetricsFactory(Config metricsConfigNode) { * @return current metrics factory */ static MetricsFactory getMetricsFactory() { - return access(() -> { - metricsConfigNode = Objects.requireNonNullElseGet(metricsConfigNode, - MetricsFactoryManager::externalMetricsConfig); - metricsFactory = Objects.requireNonNullElseGet(metricsFactory, - () -> getMetricsFactory(metricsConfigNode)); - return metricsFactory; + return METRICS_FACTORY.get(() -> { + Config metricsConfig = METRICS_CONFIG.get(() -> new MetricsConfigHolder(MetricsFactoryManager + .externalMetricsConfig())) + .config(); + return buildMetricsFactory(metricsConfig); }); } @@ -142,12 +137,21 @@ static MetricsFactory create(Config metricsConfigNode) { return METRICS_FACTORY_PROVIDER.get().create(metricsConfigNode, MetricsConfig.create( metricsConfigNode.get(MetricsConfig.METRICS_CONFIG_KEY)), - METER_PROVIDERS.get()); + METER_PROVIDERS.get().providers()); } static void closeAll() { METRICS_FACTORY_PROVIDER.get().close(); - metricsFactory = null; + METRICS_FACTORY.value() + .ifPresent(MetricsFactory::close); + } + + private static MetricsFactory buildMetricsFactory(Config metricsConfigNode) { + MetricsConfig metricsConfig = applyOverrides(MetricsConfig.create(metricsConfigNode)); + SystemTagsManager.instance(metricsConfig); + + return METRICS_FACTORY_PROVIDER.get() + .create(metricsConfigNode, metricsConfig, METER_PROVIDERS.get().providers()); } private static Config externalMetricsConfig() { @@ -158,30 +162,15 @@ private static Config externalMetricsConfig() { return serverFeaturesMetricsConfig; } - private static MetricsFactory completeGetInstance(MetricsConfig metricsConfig, Config metricsConfigNode) { - - metricsConfig = applyOverrides(metricsConfig); - - SystemTagsManager.instance(metricsConfig); - metricsFactory = METRICS_FACTORY_PROVIDER.get().create(metricsConfigNode, metricsConfig, METER_PROVIDERS.get()); - - return metricsFactory; - } - private static MetricsConfig applyOverrides(MetricsConfig metricsConfig) { MetricsConfig.Builder metricsConfigBuilder = MetricsConfig.builder(metricsConfig); METRICS_CONFIG_OVERRIDES.get().forEach(override -> override.apply(metricsConfigBuilder)); return metricsConfigBuilder.build(); } - private static T access(Callable c) { - LOCK.lock(); - try { - return c.call(); - } catch (Exception e) { - throw new RuntimeException(e); - } finally { - LOCK.unlock(); - } + private record MetersProviders(Collection providers) { + } + + private record MetricsConfigHolder(Config config) { } } diff --git a/metrics/api/src/main/java/module-info.java b/metrics/api/src/main/java/module-info.java index 06ee5f35559..20e16a5340a 100644 --- a/metrics/api/src/main/java/module-info.java +++ b/metrics/api/src/main/java/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2023 Oracle and/or its affiliates. + * Copyright (c) 2021, 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. @@ -27,12 +27,13 @@ )module io.helidon.metrics.api { requires static io.helidon.common.features.api; + requires static io.helidon.config.metadata; + requires io.helidon.builder.api; requires io.helidon.http; - requires transitive io.helidon.common.config; - requires io.helidon.builder.api; - requires static io.helidon.config.metadata; + requires transitive io.helidon.common.config; + requires io.helidon.common.context; exports io.helidon.metrics.api; exports io.helidon.metrics.spi; diff --git a/metrics/provider-tests/pom.xml b/metrics/provider-tests/pom.xml index efb9dce4511..282c9577916 100644 --- a/metrics/provider-tests/pom.xml +++ b/metrics/provider-tests/pom.xml @@ -63,6 +63,10 @@ io.micrometer micrometer-core
+ + io.helidon.testing + helidon-testing-junit5 + diff --git a/metrics/provider-tests/src/main/java/io/helidon/metrics/provider/tests/SimpleMeterRegistryTests.java b/metrics/provider-tests/src/main/java/io/helidon/metrics/provider/tests/SimpleMeterRegistryTests.java index 86a93dcf45e..8aaba38f1ad 100644 --- a/metrics/provider-tests/src/main/java/io/helidon/metrics/provider/tests/SimpleMeterRegistryTests.java +++ b/metrics/provider-tests/src/main/java/io/helidon/metrics/provider/tests/SimpleMeterRegistryTests.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 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. @@ -22,6 +22,7 @@ import io.helidon.metrics.api.Metrics; import io.helidon.metrics.api.Tag; import io.helidon.metrics.api.Timer; +import io.helidon.testing.junit5.Testing; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -35,6 +36,7 @@ import static org.hamcrest.Matchers.sameInstance; import static org.junit.jupiter.api.Assertions.assertThrows; +@Testing.Test class SimpleMeterRegistryTests { private static MeterRegistry meterRegistry; diff --git a/metrics/provider-tests/src/main/java/io/helidon/metrics/provider/tests/TestCounter.java b/metrics/provider-tests/src/main/java/io/helidon/metrics/provider/tests/TestCounter.java index cbf91268a08..50f65ac8456 100644 --- a/metrics/provider-tests/src/main/java/io/helidon/metrics/provider/tests/TestCounter.java +++ b/metrics/provider-tests/src/main/java/io/helidon/metrics/provider/tests/TestCounter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 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. @@ -18,6 +18,7 @@ import io.helidon.metrics.api.Counter; import io.helidon.metrics.api.MeterRegistry; import io.helidon.metrics.api.Metrics; +import io.helidon.testing.junit5.Testing; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -26,6 +27,7 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.sameInstance; +@Testing.Test class TestCounter { private static MeterRegistry meterRegistry; diff --git a/metrics/provider-tests/src/main/java/io/helidon/metrics/provider/tests/TestDeletions.java b/metrics/provider-tests/src/main/java/io/helidon/metrics/provider/tests/TestDeletions.java index d4a29a8aed4..89873c81b50 100644 --- a/metrics/provider-tests/src/main/java/io/helidon/metrics/provider/tests/TestDeletions.java +++ b/metrics/provider-tests/src/main/java/io/helidon/metrics/provider/tests/TestDeletions.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 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. @@ -24,6 +24,7 @@ import io.helidon.metrics.api.MeterRegistry; import io.helidon.metrics.api.Metrics; import io.helidon.metrics.api.Tag; +import io.helidon.testing.junit5.Testing; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; @@ -33,6 +34,7 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.sameInstance; +@Testing.Test class TestDeletions { private static final String COMMON_COUNTER_NAME = "theCounter"; diff --git a/metrics/provider-tests/src/main/java/io/helidon/metrics/provider/tests/TestDistributionSummary.java b/metrics/provider-tests/src/main/java/io/helidon/metrics/provider/tests/TestDistributionSummary.java index 9de795df4d9..7d628e51f02 100644 --- a/metrics/provider-tests/src/main/java/io/helidon/metrics/provider/tests/TestDistributionSummary.java +++ b/metrics/provider-tests/src/main/java/io/helidon/metrics/provider/tests/TestDistributionSummary.java @@ -26,6 +26,7 @@ import io.helidon.metrics.api.MeterRegistry; import io.helidon.metrics.api.Metrics; import io.helidon.metrics.api.ValueAtPercentile; +import io.helidon.testing.junit5.Testing; import org.hamcrest.Description; import org.hamcrest.Matcher; @@ -39,6 +40,7 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; +@Testing.Test class TestDistributionSummary { private static MeterRegistry meterRegistry; diff --git a/metrics/provider-tests/src/main/java/io/helidon/metrics/provider/tests/TestGauge.java b/metrics/provider-tests/src/main/java/io/helidon/metrics/provider/tests/TestGauge.java index 9c322f16fbd..98e04e84735 100644 --- a/metrics/provider-tests/src/main/java/io/helidon/metrics/provider/tests/TestGauge.java +++ b/metrics/provider-tests/src/main/java/io/helidon/metrics/provider/tests/TestGauge.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 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. @@ -20,6 +20,7 @@ import io.helidon.metrics.api.Gauge; import io.helidon.metrics.api.MeterRegistry; import io.helidon.metrics.api.Metrics; +import io.helidon.testing.junit5.Testing; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -27,6 +28,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; +@Testing.Test class TestGauge { private static MeterRegistry meterRegistry; diff --git a/metrics/provider-tests/src/main/java/io/helidon/metrics/provider/tests/TestGlobalTags.java b/metrics/provider-tests/src/main/java/io/helidon/metrics/provider/tests/TestGlobalTags.java index 1a810fe2cc0..b9e23484824 100644 --- a/metrics/provider-tests/src/main/java/io/helidon/metrics/provider/tests/TestGlobalTags.java +++ b/metrics/provider-tests/src/main/java/io/helidon/metrics/provider/tests/TestGlobalTags.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 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. @@ -26,6 +26,7 @@ import io.helidon.metrics.api.MetricsConfig; import io.helidon.metrics.api.MetricsFactory; import io.helidon.metrics.api.Tag; +import io.helidon.testing.junit5.Testing; import org.junit.jupiter.api.Test; @@ -34,6 +35,7 @@ import static org.hamcrest.Matchers.hasItems; import static org.hamcrest.Matchers.not; +@Testing.Test class TestGlobalTags { @Test diff --git a/metrics/provider-tests/src/main/java/io/helidon/metrics/provider/tests/TestScopeManagement.java b/metrics/provider-tests/src/main/java/io/helidon/metrics/provider/tests/TestScopeManagement.java index 83e1cb6807f..2393c0debf4 100644 --- a/metrics/provider-tests/src/main/java/io/helidon/metrics/provider/tests/TestScopeManagement.java +++ b/metrics/provider-tests/src/main/java/io/helidon/metrics/provider/tests/TestScopeManagement.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 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. @@ -29,6 +29,7 @@ import io.helidon.metrics.api.ScopingConfig; import io.helidon.metrics.api.SystemTagsManager; import io.helidon.metrics.api.Timer; +import io.helidon.testing.junit5.Testing; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -40,6 +41,7 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; +@Testing.Test class TestScopeManagement { @ParameterizedTest diff --git a/metrics/provider-tests/src/main/java/io/helidon/metrics/provider/tests/TestTimer.java b/metrics/provider-tests/src/main/java/io/helidon/metrics/provider/tests/TestTimer.java index be390872209..ad941450459 100644 --- a/metrics/provider-tests/src/main/java/io/helidon/metrics/provider/tests/TestTimer.java +++ b/metrics/provider-tests/src/main/java/io/helidon/metrics/provider/tests/TestTimer.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 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. @@ -26,6 +26,7 @@ import io.helidon.metrics.api.MetricsConfig; import io.helidon.metrics.api.MetricsFactory; import io.helidon.metrics.api.Timer; +import io.helidon.testing.junit5.Testing; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -35,6 +36,7 @@ import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.fail; +@Testing.Test class TestTimer { private static MeterRegistry meterRegistry; diff --git a/metrics/provider-tests/src/main/java/module-info.java b/metrics/provider-tests/src/main/java/module-info.java index 398ad0c4219..a3b7e75606c 100644 --- a/metrics/provider-tests/src/main/java/module-info.java +++ b/metrics/provider-tests/src/main/java/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 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. @@ -27,4 +27,7 @@ requires io.helidon.config; requires org.junit.jupiter.params; requires micrometer.core; + requires io.helidon.testing.junit5; + + exports io.helidon.metrics.provider.tests; } \ No newline at end of file diff --git a/metrics/providers/micrometer/pom.xml b/metrics/providers/micrometer/pom.xml index 89847d75cce..aafc77dce72 100644 --- a/metrics/providers/micrometer/pom.xml +++ b/metrics/providers/micrometer/pom.xml @@ -61,6 +61,11 @@ hamcrest-all test + + io.helidon.logging + helidon-logging-jul + test + io.prometheus simpleclient_tracer_common @@ -73,11 +78,11 @@ org.apache.maven.plugins maven-surefire-plugin - - - junit.jupiter.extensions.autodetection.enabled = true - - + classes + 5 + + true + diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/providers/micrometer/MMeterRegistry.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/providers/micrometer/MMeterRegistry.java index 201d4e5b3c8..7f7846cfc13 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/providers/micrometer/MMeterRegistry.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/providers/micrometer/MMeterRegistry.java @@ -127,88 +127,6 @@ static Builder builder( return new Builder(delegate, metricsFactory); } - /** - * Creates a new meter registry which wraps an newly-created Micrometer - * {@link io.micrometer.core.instrument.composite.CompositeMeterRegistry} with a Prometheus meter registry - * automatically added, using the specified clock. - * - * @param metricsFactory metrics factory the new meter registry should use in creating and registering meters - * @param clock default clock to associate with the new meter registry - * @param metersProviders providers of built-in meters to be registered upon creation of the meter registry - * @return new wrapper around a new Micrometer composite meter registry - */ - static MMeterRegistry create(MicrometerMetricsFactory metricsFactory, - Clock clock, - Collection metersProviders) { - CompositeMeterRegistry delegate = new CompositeMeterRegistry(ClockWrapper.create(clock)); - // The specified clock is already a Helidon one so pass it directly; no need to wrap it. - return create(delegate, - metricsFactory, - metricsFactory.metricsConfig(), - clock, - metersProviders); - } - - static MMeterRegistry create(io.micrometer.core.instrument.MeterRegistry delegate, - MicrometerMetricsFactory metricsFactory, - MetricsConfig metricsConfig, - Collection metersProviders) { - - return create(delegate, metricsFactory, metricsConfig, MClock.create(delegate.config().clock()), metersProviders); - } - - static MMeterRegistry create(io.micrometer.core.instrument.MeterRegistry delegate, - MicrometerMetricsFactory metricsFactory, - MetricsConfig metricsConfig, - Collection metersProviders, - Consumer onAddListener, - Consumer onRemoveListener) { - MMeterRegistry result = create(delegate, metricsFactory, metricsConfig, MClock.create(delegate.config().clock())); - result.onMeterAdded(onAddListener) - .onMeterRemoved(onRemoveListener); - return applyMetersProvidersToRegistry(metricsFactory, result, metersProviders); - } - - static MMeterRegistry create(io.micrometer.core.instrument.MeterRegistry delegate, - MicrometerMetricsFactory metricsFactory, - MetricsConfig metricsConfig, - Clock clock, - Collection metersProviders) { - - return applyMetersProvidersToRegistry(metricsFactory, - create(delegate, - metricsFactory, - metricsConfig, - clock), - metersProviders); - } - - static MMeterRegistry create(io.micrometer.core.instrument.MeterRegistry delegate, - MicrometerMetricsFactory metricsFactory, - MetricsConfig metricsConfig, - Clock clock) { - - io.micrometer.core.instrument.MeterRegistry preppedDelegate = - ensurePrometheusRegistryIsPresent(delegate, - metricsFactory.metricsConfig()); - - return new MMeterRegistry(preppedDelegate, - metricsFactory, - metricsConfig, - clock); - } - - static MMeterRegistry create(io.micrometer.core.instrument.MeterRegistry delegate, - MicrometerMetricsFactory metricsFactory, - Collection metersProviders) { - - return create(delegate, - metricsFactory, - metricsFactory.metricsConfig(), - MClock.create(delegate.config().clock()), - metersProviders); - } - static MMeterRegistry applyMetersProvidersToRegistry(MetricsFactory factory, MMeterRegistry registry, Collection metersProviders) { @@ -230,6 +148,7 @@ public void close() { buildersByPromMeterId.clear(); scopeMembership.clear(); metersById.clear(); + delegate.clear(); } finally { lock.writeLock().unlock(); } @@ -558,7 +477,7 @@ void onMeterRemoved(Meter removedMeter) { private io.helidon.metrics.api.Meter noopMeterIfDisabled(io.helidon.metrics.api.Meter.Builder builder) { if (!isMeterEnabled(builder.name(), builder.tags(), builder.scope())) { - + System.out.println("Meter is disabled: " + builder.name()); io.helidon.metrics.api.Meter result = metricsFactory.noOpMeter(builder); onAddListeners.forEach(listener -> listener.accept(result)); return result; @@ -594,6 +513,7 @@ private static io.micrometer.core.instrument.MeterRegistry ensurePrometheusRegis MetricsConfig metricsConfig) { if (meterRegistry instanceof CompositeMeterRegistry compositeMeterRegistry) { + if (compositeMeterRegistry.getRegistries() .stream() .noneMatch(r -> r instanceof PrometheusMeterRegistry)) { @@ -894,30 +814,4 @@ public R build() { return (R) applyMetersProvidersToRegistry(metricsFactory, result, metersProviders); } } - - /** - * Micrometer-friendly wrapper around a Helidon clock. - */ - private static class ClockWrapper implements io.micrometer.core.instrument.Clock { - - private final Clock neutralClock; - - private ClockWrapper(Clock neutralClock) { - this.neutralClock = neutralClock; - } - - static ClockWrapper create(Clock clock) { - return new ClockWrapper(clock); - } - - @Override - public long wallTime() { - return neutralClock.wallTime(); - } - - @Override - public long monotonicTime() { - return neutralClock.monotonicTime(); - } - } } diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/providers/micrometer/MicrometerMetricsFactory.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/providers/micrometer/MicrometerMetricsFactory.java index 241af47bb29..7aebb598719 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/providers/micrometer/MicrometerMetricsFactory.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/providers/micrometer/MicrometerMetricsFactory.java @@ -204,6 +204,9 @@ public void close() { registries.forEach(MMeterRegistry::close); meterRegistries.clear(); globalMeterRegistry = null; + for (var registry : Metrics.globalRegistry.getRegistries()) { + Metrics.globalRegistry.remove(registry); + } } @Override diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/providers/micrometer/MicrometerMetricsFactoryProvider.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/providers/micrometer/MicrometerMetricsFactoryProvider.java index bd98cb6821e..2dbec24276d 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/providers/micrometer/MicrometerMetricsFactoryProvider.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/providers/micrometer/MicrometerMetricsFactoryProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 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. @@ -51,7 +51,8 @@ public MetricsFactory create(Config rootConfig, MetricsConfig metricsConfig, Col @Override public void close() { - metricsFactories.forEach(MetricsFactory::close); + var toHandle = List.copyOf(metricsFactories); + toHandle.forEach(MetricsFactory::close); metricsFactories.clear(); List meters = List.copyOf(Metrics.globalRegistry.getMeters()); meters.forEach(Metrics.globalRegistry::remove); diff --git a/metrics/providers/micrometer/src/test/java/io/helidon/metrics/providers/micrometer/MicrometerMetricsTestsJunitExtension.java b/metrics/providers/micrometer/src/test/java/io/helidon/metrics/providers/micrometer/MicrometerMetricsTestsJunitExtension.java index 3deba89e1ab..2b5ed4d1fee 100644 --- a/metrics/providers/micrometer/src/test/java/io/helidon/metrics/providers/micrometer/MicrometerMetricsTestsJunitExtension.java +++ b/metrics/providers/micrometer/src/test/java/io/helidon/metrics/providers/micrometer/MicrometerMetricsTestsJunitExtension.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 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. @@ -17,17 +17,36 @@ import java.util.concurrent.TimeUnit; +import io.helidon.common.context.Context; +import io.helidon.common.context.Contexts; import io.helidon.metrics.api.MetricsFactory; +import io.helidon.testing.junit5.TestJunitExtension; +import org.junit.jupiter.api.extension.AfterAllCallback; import org.junit.jupiter.api.extension.BeforeAllCallback; import org.junit.jupiter.api.extension.Extension; import org.junit.jupiter.api.extension.ExtensionContext; -public class MicrometerMetricsTestsJunitExtension implements Extension, - BeforeAllCallback { +public class MicrometerMetricsTestsJunitExtension extends TestJunitExtension implements Extension, + BeforeAllCallback, + AfterAllCallback { - static void clear() { + public MicrometerMetricsTestsJunitExtension() { + } + + @Override + public void beforeAll(ExtensionContext extensionContext) { + super.beforeAll(extensionContext); + super.run(extensionContext, this::clear); + } + + @Override + public void afterAll(ExtensionContext context) { + super.run(context, this::clear); + super.afterAll(context); + } + void clear() { MetricsFactory.closeAll(); // And clear out Micrometer's global registry explicitly to be extra sure. @@ -47,9 +66,4 @@ static void clear() { } } } - - @Override - public void beforeAll(ExtensionContext extensionContext) throws Exception { - clear(); - } } diff --git a/metrics/providers/micrometer/src/test/java/io/helidon/metrics/providers/micrometer/TestCounter.java b/metrics/providers/micrometer/src/test/java/io/helidon/metrics/providers/micrometer/TestCounter.java index f1f0152df33..66586ef1fa4 100644 --- a/metrics/providers/micrometer/src/test/java/io/helidon/metrics/providers/micrometer/TestCounter.java +++ b/metrics/providers/micrometer/src/test/java/io/helidon/metrics/providers/micrometer/TestCounter.java @@ -15,15 +15,11 @@ */ package io.helidon.metrics.providers.micrometer; -import java.util.List; - import io.helidon.common.testing.junit5.OptionalMatcher; import io.helidon.metrics.api.Counter; import io.helidon.metrics.api.MeterRegistry; import io.helidon.metrics.api.Metrics; -import io.helidon.metrics.api.MetricsConfig; -import io.micrometer.core.instrument.Meter; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; diff --git a/metrics/providers/micrometer/src/test/java/io/helidon/metrics/providers/micrometer/TestGauge.java b/metrics/providers/micrometer/src/test/java/io/helidon/metrics/providers/micrometer/TestGauge.java index 83be62a2103..af115e32dfe 100644 --- a/metrics/providers/micrometer/src/test/java/io/helidon/metrics/providers/micrometer/TestGauge.java +++ b/metrics/providers/micrometer/src/test/java/io/helidon/metrics/providers/micrometer/TestGauge.java @@ -20,7 +20,6 @@ import io.helidon.metrics.api.Gauge; import io.helidon.metrics.api.MeterRegistry; import io.helidon.metrics.api.Metrics; -import io.helidon.metrics.api.MetricsConfig; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; diff --git a/metrics/providers/micrometer/src/test/java/io/helidon/metrics/providers/micrometer/TestIntegration.java b/metrics/providers/micrometer/src/test/java/io/helidon/metrics/providers/micrometer/TestIntegration.java index c202bb281c4..e69e02b4bdf 100644 --- a/metrics/providers/micrometer/src/test/java/io/helidon/metrics/providers/micrometer/TestIntegration.java +++ b/metrics/providers/micrometer/src/test/java/io/helidon/metrics/providers/micrometer/TestIntegration.java @@ -23,6 +23,7 @@ import org.junit.jupiter.api.Test; +import static org.hamcrest.CoreMatchers.sameInstance; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; @@ -45,6 +46,7 @@ void testHelidonRegistrationViaMicrometer() { io.micrometer.core.instrument.MeterRegistry mMeterRegistry = io.micrometer.core.instrument.Metrics.globalRegistry; io.micrometer.core.instrument.Counter mCounter = mMeterRegistry.counter("hCounter1", "scope", "application"); + assertThat(unwrappedCounter, sameInstance(mCounter)); assertThat("hCounter via Micrometer meter registry", mCounter.count(), equalTo(5D)); } @@ -57,6 +59,8 @@ void testMicrometerRegistrationViaHelidon() { // Should find the previously-registered counter. Counter hCounter = hMeterRegistry.getOrCreate(Counter.builder("mCounter1")); + + assertThat(mCounter, sameInstance(hCounter.unwrap(io.micrometer.core.instrument.Counter.class))); assertThat("mCounter via Helidon with no explicit tag", hCounter.count(), equalTo(2L)); diff --git a/metrics/providers/micrometer/src/test/java/io/helidon/metrics/providers/micrometer/TestPrometheusFormatting.java b/metrics/providers/micrometer/src/test/java/io/helidon/metrics/providers/micrometer/TestPrometheusFormatting.java index 10a98c31036..1328670ec72 100644 --- a/metrics/providers/micrometer/src/test/java/io/helidon/metrics/providers/micrometer/TestPrometheusFormatting.java +++ b/metrics/providers/micrometer/src/test/java/io/helidon/metrics/providers/micrometer/TestPrometheusFormatting.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 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. @@ -52,8 +52,8 @@ Only OpenMetrics format, not the Prometheus exposition format, has the trailing media type. */ private static final String OPENMETRICS_EOF = "# EOF\n"; - private static MeterRegistry meterRegistry; + private static MeterRegistry meterRegistry; private static MetricsConfig metricsConfig; @BeforeAll @@ -73,7 +73,6 @@ static void prep() { * When a metrics factory is obtained via MetricsFactory.getInstance(metricsConfig), that config object initializes * the system tags manager. This happens after the @BeforeAll method runs. So re-assert the values we want for the * test here. We would only need to do it once, not before each test, but it's low cost esp. in a test environment. - * *

*/ @BeforeEach diff --git a/metrics/providers/micrometer/src/test/java/io/helidon/metrics/providers/micrometer/TestTimer.java b/metrics/providers/micrometer/src/test/java/io/helidon/metrics/providers/micrometer/TestTimer.java index 4b32aa3a454..aa58bfec1e9 100644 --- a/metrics/providers/micrometer/src/test/java/io/helidon/metrics/providers/micrometer/TestTimer.java +++ b/metrics/providers/micrometer/src/test/java/io/helidon/metrics/providers/micrometer/TestTimer.java @@ -20,7 +20,6 @@ import io.helidon.metrics.api.MeterRegistry; import io.helidon.metrics.api.Metrics; -import io.helidon.metrics.api.MetricsConfig; import io.helidon.metrics.api.Timer; import org.junit.jupiter.api.BeforeAll; diff --git a/metrics/providers/micrometer/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension b/metrics/providers/micrometer/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension index 9e73d6bad71..e7b2b6418bf 100644 --- a/metrics/providers/micrometer/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension +++ b/metrics/providers/micrometer/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension @@ -1 +1 @@ -io.helidon.metrics.providers.micrometer.MicrometerMetricsTestsJunitExtension \ No newline at end of file +io.helidon.metrics.providers.micrometer.MicrometerMetricsTestsJunitExtension diff --git a/microprofile/cdi/src/main/java/io/helidon/microprofile/cdi/HelidonContainerImpl.java b/microprofile/cdi/src/main/java/io/helidon/microprofile/cdi/HelidonContainerImpl.java index 03670cd4dd4..7ebf2aab297 100644 --- a/microprofile/cdi/src/main/java/io/helidon/microprofile/cdi/HelidonContainerImpl.java +++ b/microprofile/cdi/src/main/java/io/helidon/microprofile/cdi/HelidonContainerImpl.java @@ -93,7 +93,6 @@ final class HelidonContainerImpl extends Weld implements HelidonContainer { private static final AtomicBoolean IN_RUNTIME = new AtomicBoolean(); private static final String EXIT_ON_STARTED_KEY = "exit.on.started"; private static final boolean EXIT_ON_STARTED = "!".equals(System.getProperty(EXIT_ON_STARTED_KEY)); - private static final Context ROOT_CONTEXT; // whether the current shutdown was invoked by the shutdown hook private static final AtomicBoolean FROM_SHUTDOWN_HOOK = new AtomicBoolean(); // Default Weld container id. TCKs assumes this value. @@ -102,26 +101,25 @@ final class HelidonContainerImpl extends Weld implements HelidonContainer { static { HelidonFeatures.flavor(HelidonFlavor.MP); - Context.Builder contextBuilder = Context.builder() - .id("helidon-cdi"); - - Contexts.context() - .ifPresent(contextBuilder::parent); - - ROOT_CONTEXT = contextBuilder.build(); - CDI.setCDIProvider(new HelidonCdiProvider()); } private static volatile HelidonShutdownHandler shutdownHandler; private final WeldBootstrap bootstrap; private final String id; + private final Context rootContext; private HelidonCdi cdi; HelidonContainerImpl() { this.bootstrap = new WeldBootstrap(); this.id = STATIC_INSTANCE; + this.rootContext = Context.builder() + .id("helidon-cdi") + .update(it -> + Contexts.context() + .ifPresent(it::parent)) + .build(); } /** @@ -140,7 +138,7 @@ static HelidonContainerImpl create() { void initInContext() { long time = System.nanoTime(); - Contexts.runInContext(ROOT_CONTEXT, this::init); + Contexts.runInContext(rootContext, this::init); time = System.nanoTime() - time; long t = TimeUnit.MILLISECONDS.convert(time, TimeUnit.NANOSECONDS); @@ -255,7 +253,7 @@ public SeContainer start() { SerializationConfig.configureRuntime(); LogConfig.configureRuntime(); try { - Contexts.runInContext(ROOT_CONTEXT, this::doStart); + Contexts.runInContext(rootContext, this::doStart); } catch (Exception e) { try { // we must clean up @@ -274,7 +272,7 @@ public SeContainer start() { @Override public Context context() { - return ROOT_CONTEXT; + return rootContext; } @Override diff --git a/microprofile/testing/junit5/pom.xml b/microprofile/testing/junit5/pom.xml index 788f1f11861..c51b5eda503 100644 --- a/microprofile/testing/junit5/pom.xml +++ b/microprofile/testing/junit5/pom.xml @@ -43,6 +43,10 @@ helidon-microprofile-cdi provided
+ + io.helidon.testing + helidon-testing-junit5 + org.glassfish.jersey.ext.cdi jersey-weld2-se diff --git a/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/HelidonJunitExtension.java b/microprofile/testing/junit5/src/main/java/io/helidon/microprofile/testing/junit5/HelidonJunitExtension.java index 5599a1bc9fe..2205d3ae50e 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 @@ -36,9 +36,11 @@ import java.util.Map; import java.util.Set; +import io.helidon.common.context.Context; import io.helidon.config.mp.MpConfigSources; import io.helidon.microprofile.server.JaxRsCdiExtension; import io.helidon.microprofile.server.ServerCdiExtension; +import io.helidon.testing.junit5.TestJunitExtension; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.Dependent; @@ -77,16 +79,16 @@ import org.junit.jupiter.api.extension.ParameterResolver; import org.junit.jupiter.api.extension.ReflectiveInvocationContext; - /** * Junit5 extension to support Helidon CDI container in tests. */ -class HelidonJunitExtension implements BeforeAllCallback, - AfterAllCallback, - BeforeEachCallback, - AfterEachCallback, - InvocationInterceptor, - ParameterResolver { +class HelidonJunitExtension extends TestJunitExtension + implements BeforeAllCallback, + AfterAllCallback, + BeforeEachCallback, + AfterEachCallback, + InvocationInterceptor, + ParameterResolver { private static final Set> HELIDON_TEST_ANNOTATIONS = Set.of(AddBean.class, AddConfig.class, AddExtension.class, Configuration.class, AddJaxRs.class); private static final Map, Annotation> BEAN_DEFINING = new HashMap<>(); @@ -109,61 +111,221 @@ class HelidonJunitExtension implements BeforeAllCallback, private Config config; private SeContainer container; - - @SuppressWarnings("unchecked") @Override public void beforeAll(ExtensionContext context) { - testClass = context.getRequiredTestClass(); + super.beforeAll(context); - AddConfig[] configs = getAnnotations(testClass, AddConfig.class); - classLevelConfigMeta.addConfig(configs); - classLevelConfigMeta.configuration(testClass.getAnnotation(Configuration.class)); - classLevelConfigMeta.addConfigBlock(testClass.getAnnotation(AddConfigBlock.class)); - configProviderResolver = ConfigProviderResolver.instance(); + run(context, () -> { + testClass = context.getRequiredTestClass(); - AddExtension[] extensions = getAnnotations(testClass, AddExtension.class); - classLevelExtensions.addAll(Arrays.asList(extensions)); + AddConfig[] configs = getAnnotations(testClass, AddConfig.class); + classLevelConfigMeta.addConfig(configs); + classLevelConfigMeta.configuration(testClass.getAnnotation(Configuration.class)); + classLevelConfigMeta.addConfigBlock(testClass.getAnnotation(AddConfigBlock.class)); + configProviderResolver = ConfigProviderResolver.instance(); - AddBean[] beans = getAnnotations(testClass, AddBean.class); - classLevelBeans.addAll(Arrays.asList(beans)); + AddExtension[] extensions = getAnnotations(testClass, AddExtension.class); + classLevelExtensions.addAll(Arrays.asList(extensions)); - HelidonTest testAnnot = testClass.getAnnotation(HelidonTest.class); - if (testAnnot != null) { - resetPerTest = testAnnot.resetPerTest(); - } + AddBean[] beans = getAnnotations(testClass, AddBean.class); + classLevelBeans.addAll(Arrays.asList(beans)); - DisableDiscovery discovery = testClass.getAnnotation(DisableDiscovery.class); - if (discovery != null) { - classLevelDisableDiscovery = discovery.value(); - } + HelidonTest testAnnot = testClass.getAnnotation(HelidonTest.class); + if (testAnnot != null) { + resetPerTest = testAnnot.resetPerTest(); + } + + DisableDiscovery discovery = testClass.getAnnotation(DisableDiscovery.class); + if (discovery != null) { + classLevelDisableDiscovery = discovery.value(); + } + + if (resetPerTest) { + validatePerTest(); + + return; + } + validatePerClass(); + + // add beans when using JaxRS + AddJaxRs addJaxRsAnnotation = testClass.getAnnotation(AddJaxRs.class); + 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); + } + }); + } + + @Override + public void beforeEach(ExtensionContext context) { if (resetPerTest) { - validatePerTest(); + Method method = context.getRequiredTestMethod(); - return; - } - validatePerClass(); + Context helidonContext = Context.builder() + .id("test-" + testClass.getName() + "-" + System.identityHashCode(testClass) + + "-" + System.identityHashCode(method)) + .build(); + super.context(context, helidonContext); - // add beans when using JaxRS - AddJaxRs addJaxRsAnnotation = testClass.getAnnotation(AddJaxRs.class); - if (addJaxRsAnnotation != null){ - classLevelExtensions.add(ProcessAllAnnotatedTypesLiteral.INSTANCE); - classLevelExtensions.add(ServerCdiExtensionLiteral.INSTANCE); - classLevelExtensions.add(JaxRsCdiExtensionLiteral.INSTANCE); - classLevelExtensions.add(CdiComponentProviderLiteral.INSTANCE); - classLevelBeans.add(WeldRequestScopeLiteral.INSTANCE); - } + 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)); - configure(classLevelConfigMeta); + AddBean[] beans = method.getAnnotationsByType(AddBean.class); + methodLevelBeans.addAll(Arrays.asList(beans)); - 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); + DisableDiscovery discovery = method.getAnnotation(DisableDiscovery.class); + if (discovery != null) { + methodLevelDisableDiscovery = discovery.value(); + } + + startContainer(methodLevelBeans, methodLevelExtensions, methodLevelDisableDiscovery); + }); } } + @Override + public void afterEach(ExtensionContext context) { + run(context, () -> { + if (resetPerTest) { + releaseConfig(); + stopContainer(); + } + }); + } + + @Override + public void afterAll(ExtensionContext context) { + run(context, () -> { + stopContainer(); + releaseConfig(); + callAfterStop(); + }); + super.afterAll(context); + } + + @Override + public T interceptTestClassConstructor(Invocation invocation, + ReflectiveInvocationContext> invocationContext, + ExtensionContext extensionContext) throws Throwable { + + return callWithThrowableInContext(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(); + + return container.select(invocationContext.getExecutable().getDeclaringClass()) + .get(); + }); + } + + @Override + public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException { + + return call(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; + }); + } + + @Override + public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException { + return call(extensionContext, () -> { + Executable executable = parameterContext.getParameter().getDeclaringExecutable(); + Class paramType = parameterContext.getParameter().getType(); + + 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); + } else { + return null; + } + }); + } + @SuppressWarnings("unchecked") private T[] getAnnotations(Class testClass, Class annotClass) { // inherited does not help, as it only returns annot from superclass if @@ -181,51 +343,12 @@ private T[] getAnnotations(Class testClass, Class a Object result = Array.newInstance(annotClass, allAnnotations.size()); for (int i = 0; i < allAnnotations.size(); i++) { - Array.set(result, i, allAnnotations.get(i)); + Array.set(result, i, allAnnotations.get(i)); } return (T[]) result; } - @Override - public void beforeEach(ExtensionContext context) throws Exception { - if (resetPerTest) { - Method method = context.getRequiredTestMethod(); - 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); - } - } - - @Override - public void afterEach(ExtensionContext context) throws Exception { - if (resetPerTest) { - releaseConfig(); - stopContainer(); - } - } - private void validatePerClass() { Method[] methods = testClass.getMethods(); for (Method method : methods) { @@ -254,8 +377,8 @@ private void validatePerClass() { } AddJaxRs addJaxRsAnnotation = testClass.getAnnotation(AddJaxRs.class); - if (addJaxRsAnnotation != null){ - if (testClass.getAnnotation(DisableDiscovery.class) == null){ + if (addJaxRsAnnotation != null) { + if (testClass.getAnnotation(DisableDiscovery.class) == null) { throw new RuntimeException("@AddJaxRs annotation should be used only with @DisableDiscovery annotation."); } } @@ -337,6 +460,7 @@ private void configure(ConfigMeta configMeta) { configProviderResolver.registerConfig(config, Thread.currentThread().getContextClassLoader()); } } + private void releaseConfig() { if (configProviderResolver != null && config != null) { configProviderResolver.releaseConfig(config); @@ -378,104 +502,6 @@ private void stopContainer() { } } - @Override - public void afterAll(ExtensionContext context) { - stopContainer(); - releaseConfig(); - callAfterStop(); - } - - @Override - public T interceptTestClassConstructor(Invocation invocation, - ReflectiveInvocationContext> invocationContext, - ExtensionContext extensionContext) throws Throwable { - - 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(); - - return container.select(invocationContext.getExecutable().getDeclaringClass()) - .get(); - } - - @Override - public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) - throws ParameterResolutionException { - - 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; - } - - @Override - public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) - throws ParameterResolutionException { - Executable executable = parameterContext.getParameter().getDeclaringExecutable(); - Class paramType = parameterContext.getParameter().getType(); - - 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); - } else { - return null; - } - } - private void callAfterStop() { List toInvoke = new ArrayList<>(); @@ -517,17 +543,16 @@ private AddBeansExtension(Class testClass, List addBeans) { this.addBeans = addBeans; } - - void processSocketInjectionPoints(@Observes ProcessInjectionPoint event) throws Exception{ - 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 processSocketInjectionPoints(@Observes ProcessInjectionPoint event) throws Exception { + 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; } + } } @@ -552,21 +577,6 @@ void registerOtherBeans(@Observes AfterBeanDiscovery event) { } - @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); @@ -591,6 +601,21 @@ void registerAddedBeans(@Observes BeforeBeanDiscovery event) { } } + @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"); + } + } + private boolean hasBda(Class value) { // does it have bean defining annotation? for (Class aClass : BEAN_DEFINING.keySet()) { @@ -624,6 +649,17 @@ private ConfigMeta() { additionalKeys.put("mp.config.profile", "test"); } + 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; + } + private void addConfig(AddConfig[] configs) { for (AddConfig config : configs) { additionalKeys.put(config.key(), config.value()); @@ -648,20 +684,8 @@ private void addConfigBlock(AddConfigBlock config) { 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}. */ @@ -683,7 +707,6 @@ public Class scope() { } } - /** * Add ProcessAllAnnotatedTypes. Used with {@code AddJaxRs}. */ diff --git a/microprofile/testing/junit5/src/main/java/module-info.java b/microprofile/testing/junit5/src/main/java/module-info.java index f9514942be0..bde39a6615b 100644 --- a/microprofile/testing/junit5/src/main/java/module-info.java +++ b/microprofile/testing/junit5/src/main/java/module-info.java @@ -22,6 +22,7 @@ requires io.helidon.config.mp; requires io.helidon.config.yaml.mp; requires io.helidon.microprofile.cdi; + requires io.helidon.testing.junit5; requires jakarta.inject; requires org.junit.jupiter.api; diff --git a/pom.xml b/pom.xml index 2e175a2ba98..9de09d63c2a 100644 --- a/pom.xml +++ b/pom.xml @@ -229,6 +229,7 @@ codegen service metadata + testing diff --git a/security/security/src/main/java/io/helidon/security/ClassToInstanceStore.java b/security/security/src/main/java/io/helidon/security/ClassToInstanceStore.java index cf5aab686d7..aba942fb252 100644 --- a/security/security/src/main/java/io/helidon/security/ClassToInstanceStore.java +++ b/security/security/src/main/java/io/helidon/security/ClassToInstanceStore.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2021 Oracle and/or its affiliates. + * Copyright (c) 2018, 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. @@ -18,8 +18,11 @@ import java.util.Collection; import java.util.IdentityHashMap; +import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; +import java.util.concurrent.locks.ReentrantReadWriteLock; /** * Map of classes to their instances. @@ -29,6 +32,7 @@ */ public final class ClassToInstanceStore { private final Map, T> backingMap = new IdentityHashMap<>(); + private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); /** * Create a new instance based on explicit instances. @@ -55,7 +59,12 @@ public static ClassToInstanceStore create(T... instances) { * @return Instance of the class or null if no mapping exists */ public Optional getInstance(Class clazz) { - return Optional.ofNullable(clazz.cast(backingMap.get(clazz))); + lock.readLock().lock(); + try { + return Optional.ofNullable(clazz.cast(backingMap.get(clazz))); + } finally { + lock.readLock().unlock(); + } } /** @@ -67,7 +76,12 @@ public Optional getInstance(Class clazz) { * @return Instance of the class if a mapping previously existed or null for no existing mapping */ public Optional putInstance(Class clazz, U instance) { - return Optional.ofNullable(clazz.cast(backingMap.put(clazz, instance))); + lock.writeLock().lock(); + try { + return Optional.ofNullable(clazz.cast(backingMap.put(clazz, instance))); + } finally { + lock.writeLock().unlock(); + } } /** @@ -78,7 +92,12 @@ public Optional putInstance(Class clazz, U instanc * @return instance that was removed (if there was one) */ public Optional removeInstance(Class clazz) { - return Optional.ofNullable(clazz.cast(backingMap.remove(clazz))); + lock.writeLock().lock(); + try { + return Optional.ofNullable(clazz.cast(backingMap.remove(clazz))); + } finally { + lock.writeLock().unlock(); + } } /** @@ -87,7 +106,12 @@ public Optional removeInstance(Class clazz) { * @param toCopy store to copy into this store */ public void putAll(ClassToInstanceStore toCopy) { - this.backingMap.putAll(toCopy.backingMap); + lock.writeLock().lock(); + try { + this.backingMap.putAll(toCopy.backingMap); + } finally { + lock.writeLock().unlock(); + } } /** @@ -97,7 +121,12 @@ public void putAll(ClassToInstanceStore toCopy) { * @return true if there is a mapping for the class */ public boolean containsKey(Class clazz) { - return backingMap.containsKey(clazz); + lock.readLock().lock(); + try { + return backingMap.containsKey(clazz); + } finally { + lock.readLock().unlock(); + } } /** @@ -106,7 +135,12 @@ public boolean containsKey(Class clazz) { * @return true if there are no mappings in this store */ public boolean isEmpty() { - return backingMap.isEmpty(); + lock.readLock().lock(); + try { + return backingMap.isEmpty(); + } finally { + lock.readLock().unlock(); + } } /** @@ -120,7 +154,12 @@ public boolean isEmpty() { */ @SuppressWarnings("unchecked") public Optional putInstance(U instance) { - return putInstance((Class) instance.getClass(), instance); + lock.writeLock().lock(); + try { + return putInstance((Class) instance.getClass(), instance); + } finally { + lock.writeLock().unlock(); + } } /** @@ -129,7 +168,12 @@ public Optional putInstance(U instance) { * @return collection of values */ public Collection values() { - return backingMap.values(); + lock.readLock().lock(); + try { + return List.copyOf(backingMap.values()); + } finally { + lock.readLock().unlock(); + } } /** @@ -138,7 +182,12 @@ public Collection values() { * @return collection of classes used for mapping to instances */ public Collection> keys() { - return backingMap.keySet(); + lock.readLock().lock(); + try { + return Set.copyOf(backingMap.keySet()); + } finally { + lock.readLock().unlock(); + } } /** @@ -148,6 +197,11 @@ public Collection> keys() { */ @Override public String toString() { - return backingMap.toString(); + lock.readLock().lock(); + try { + return backingMap.toString(); + } finally { + lock.readLock().unlock(); + } } } diff --git a/service/registry/pom.xml b/service/registry/pom.xml index 76b62d33456..7c06703834e 100644 --- a/service/registry/pom.xml +++ b/service/registry/pom.xml @@ -55,6 +55,10 @@ io.helidon.common helidon-common-config + + io.helidon.common + helidon-common-context + io.helidon.metadata helidon-metadata-hson diff --git a/service/registry/src/main/java/io/helidon/service/registry/ExistingInstanceDescriptor.java b/service/registry/src/main/java/io/helidon/service/registry/ExistingInstanceDescriptor.java new file mode 100644 index 00000000000..8ddcf1f7d4e --- /dev/null +++ b/service/registry/src/main/java/io/helidon/service/registry/ExistingInstanceDescriptor.java @@ -0,0 +1,120 @@ +/* + * 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.service.registry; + +import java.util.Collection; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +import io.helidon.common.types.TypeName; + +/** + * A special case service descriptor allowing registration of service instances that do not have + * a code generated service descriptor, such as for testing. + *

+ * Note that these instances cannot be used for creating code generated binding, as they do not exist as classes. + * @param type of the instance + */ +public final class ExistingInstanceDescriptor implements GeneratedService.Descriptor { + private static final TypeName DESCRIPTOR_TYPE = TypeName.create(ExistingInstanceDescriptor.class); + private final T instance; + private final TypeName serviceType; + private final Set contracts; + private final double weight; + + private ExistingInstanceDescriptor(T instance, + TypeName serviceType, + Set contracts, + double weight) { + this.instance = instance; + this.serviceType = serviceType; + this.contracts = contracts; + this.weight = weight; + } + + /** + * Create a new instance. + * The only place this can be used at is with + * {@link + * io.helidon.service.registry.ServiceRegistryConfig.Builder#addServiceDescriptor(io.helidon.service.registry.GeneratedService.Descriptor)}. + * + * @param instance service instance to use + * @param contracts contracts of the service (the ones we want service registry to use) + * @param weight weight of the service + * @param type of the service + * @return a new service descriptor for the provided information + */ + public static ExistingInstanceDescriptor create(T instance, + Collection> contracts, + double weight) { + TypeName serviceType = TypeName.create(instance.getClass()); + Set contractSet = contracts.stream() + .map(TypeName::create) + .collect(Collectors.toSet()); + + return new ExistingInstanceDescriptor<>(instance, serviceType, contractSet, weight); + } + + @Override + public TypeName serviceType() { + return serviceType; + } + + @Override + public TypeName descriptorType() { + return DESCRIPTOR_TYPE; + } + + @Override + public Set contracts() { + return contracts; + } + + @Override + public Object instantiate(DependencyContext ctx) { + return instance; + } + + @Override + public double weight() { + return weight; + } + + @Override + public String toString() { + return contracts + " (" + weight + ")"; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof ExistingInstanceDescriptor that)) { + return false; + } + return Double.compare(weight, that.weight) == 0 + && instance == that.instance + && Objects.equals(contracts, that.contracts); + } + + @Override + public int hashCode() { + return Objects.hash(instance, contracts, weight); + } +} 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 33e4982f209..fa76462b211 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 @@ -16,22 +16,26 @@ package io.helidon.service.registry; -import java.util.concurrent.atomic.AtomicReference; -import java.util.concurrent.locks.ReadWriteLock; -import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.function.Supplier; import io.helidon.common.config.GlobalConfig; +import io.helidon.common.context.ContextSingleton; /** * A global singleton manager for a service registry. - *

- * Note that when using this registry, testing is a bit more complicated, as the registry is shared - * statically. */ public final class GlobalServiceRegistry { - private static final AtomicReference INSTANCE = new AtomicReference<>(); - private static final ReadWriteLock RW_LOCK = new ReentrantReadWriteLock(); + private static final ContextSingleton CONTEXT_VALUE = ContextSingleton.create( + GlobalServiceRegistry.class, + ServiceRegistry.class, () -> { + ServiceRegistryConfig config; + if (GlobalConfig.configured()) { + config = ServiceRegistryConfig.create(GlobalConfig.config().get("registry")); + } else { + config = ServiceRegistryConfig.create(); + } + return ServiceRegistryManager.create(config).registry(); + }); private GlobalServiceRegistry() { } @@ -42,7 +46,7 @@ private GlobalServiceRegistry() { * @return {@code true} if a registry instance was already created */ public static boolean configured() { - return INSTANCE.get() != null; + return CONTEXT_VALUE.isPresent(); } /** @@ -51,29 +55,7 @@ public static boolean configured() { * @return global service registry */ public static ServiceRegistry registry() { - try { - RW_LOCK.readLock().lock(); - ServiceRegistry currentInstance = INSTANCE.get(); - if (currentInstance != null) { - return currentInstance; - } - } finally { - RW_LOCK.readLock().unlock(); - } - try { - RW_LOCK.writeLock().lock(); - ServiceRegistryConfig config; - if (GlobalConfig.configured()) { - config = ServiceRegistryConfig.create(GlobalConfig.config().get("registry")); - } else { - config = ServiceRegistryConfig.create(); - } - ServiceRegistry newInstance = ServiceRegistryManager.create(config).registry(); - INSTANCE.set(newInstance); - return newInstance; - } finally { - RW_LOCK.writeLock().unlock(); - } + return CONTEXT_VALUE.get(); } /** @@ -84,23 +66,7 @@ public static ServiceRegistry registry() { * @return global service registry */ public static ServiceRegistry registry(Supplier registrySupplier) { - try { - RW_LOCK.readLock().lock(); - ServiceRegistry currentInstance = INSTANCE.get(); - if (currentInstance != null) { - return currentInstance; - } - } finally { - RW_LOCK.readLock().unlock(); - } - try { - RW_LOCK.writeLock().lock(); - ServiceRegistry newInstance = registrySupplier.get(); - INSTANCE.set(newInstance); - return newInstance; - } finally { - RW_LOCK.writeLock().unlock(); - } + return CONTEXT_VALUE.get(registrySupplier); } /** @@ -112,7 +78,7 @@ public static ServiceRegistry registry(Supplier registrySupplie * @return the same instance */ public static ServiceRegistry registry(ServiceRegistry newGlobalRegistry) { - INSTANCE.set(newGlobalRegistry); + CONTEXT_VALUE.set(newGlobalRegistry); return newGlobalRegistry; } } diff --git a/service/registry/src/main/java/module-info.java b/service/registry/src/main/java/module-info.java index c387787882f..24e0c877c81 100644 --- a/service/registry/src/main/java/module-info.java +++ b/service/registry/src/main/java/module-info.java @@ -21,7 +21,7 @@ /** * Core service registry, supporting {@link io.helidon.service.registry.Service.Provider}. */ -@Feature(value = "registry", +@Feature(value = "Registry", description = "Service Registry", in = HelidonFlavor.SE, path = "Registry" @@ -36,6 +36,7 @@ requires transitive io.helidon.common.config; requires transitive io.helidon.builder.api; requires transitive io.helidon.common.types; + requires io.helidon.common.context; exports io.helidon.service.registry; exports io.helidon.service.registry.spi; diff --git a/testing/junit5/pom.xml b/testing/junit5/pom.xml new file mode 100644 index 00000000000..1dc6fd02ca9 --- /dev/null +++ b/testing/junit5/pom.xml @@ -0,0 +1,108 @@ + + + + + 4.0.0 + + io.helidon.testing + helidon-testing-project + 4.1.0-SNAPSHOT + ../pom.xml + + helidon-testing-junit5 + Helidon Testing JUnit5 + + + + io.helidon.common + helidon-common + + + io.helidon.common + helidon-common-config + + + io.helidon.common + helidon-common-context + + + io.helidon.testing + helidon-testing + + + io.helidon.logging + helidon-logging-common + + + io.helidon.service + helidon-service-registry + + + org.junit.jupiter + junit-jupiter-api + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.service + helidon-service-codegen + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-helidon-copyright + ${helidon.version} + + + + + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.service + helidon-service-codegen + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-helidon-copyright + ${helidon.version} + + + + + + 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 new file mode 100644 index 00000000000..3d8a376b8f0 --- /dev/null +++ b/testing/junit5/src/main/java/io/helidon/testing/junit5/TestJunitExtension.java @@ -0,0 +1,462 @@ +/* + * 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.testing.junit5; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.Callable; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; + +import io.helidon.common.GenericType; +import io.helidon.common.config.Config; +import io.helidon.common.config.GlobalConfig; +import io.helidon.common.context.Context; +import io.helidon.common.context.ContextSingleton; +import io.helidon.common.context.Contexts; +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.ConfigRegistrySupport; +import io.helidon.testing.TestException; +import io.helidon.testing.TestRegistry; + +import org.junit.jupiter.api.extension.AfterAllCallback; +import org.junit.jupiter.api.extension.BeforeAllCallback; +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.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; + +/** + * Helidon JUnit extension, added through {@link io.helidon.testing.junit5.Testing.Test}. + *

+ * This extension has the following features: + *

    + *
  • Run constructor and every test class method within a custom {@link io.helidon.common.context.Context}
  • + *
  • Support configuration annotations to set up configuration before running the tests
  • + *
  • Support for injection service registry (if on classpath) to discover configuration
  • + *
+ */ +public class TestJunitExtension implements Extension, + InvocationInterceptor, + BeforeAllCallback, + AfterAllCallback, + ParameterResolver { + + static { + LogConfig.initClass(); + } + + /** + * Default constructor with no side effects. + */ + 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(ContextSingleton.STATIC_CONTEXT_CLASSIFIER, helidonContext); + + ExtensionContext.Store store = extensionStore(context); + store.put(Context.class, helidonContext); + + run(context, () -> { + LogConfig.configureRuntime(); + createRegistry(store, testClass); + }); + } + + @Override + public void afterAll(ExtensionContext context) { + run(context, () -> afterShutdownMethods(context.getRequiredTestClass())); + } + + @Override + public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException { + Class paramType = parameterContext.getParameter().getType(); + if (!GenericType.create(parameterContext.getParameter().getParameterizedType()) + .isClass()) { + return false; + } + + return registrySupportedType(extensionContext, paramType); + } + + @Override + public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) + 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()); + } + + @Override + public T interceptTestClassConstructor(Invocation invocation, + ReflectiveInvocationContext> invocationContext, + ExtensionContext extensionContext) throws Throwable { + return invoke(extensionContext, invocation); + } + + @Override + public void interceptBeforeAllMethod(Invocation invocation, + ReflectiveInvocationContext invocationContext, + ExtensionContext extensionContext) throws Throwable { + invoke(extensionContext, invocation); + } + + @Override + public void interceptBeforeEachMethod(Invocation invocation, + ReflectiveInvocationContext invocationContext, + ExtensionContext extensionContext) throws Throwable { + invoke(extensionContext, invocation); + } + + @Override + public void interceptTestMethod(Invocation invocation, + ReflectiveInvocationContext invocationContext, + ExtensionContext extensionContext) throws Throwable { + invoke(extensionContext, invocation); + } + + @Override + public T interceptTestFactoryMethod(Invocation invocation, + ReflectiveInvocationContext invocationContext, + ExtensionContext extensionContext) throws Throwable { + return invoke(extensionContext, invocation); + } + + @Override + public void interceptTestTemplateMethod(Invocation invocation, + ReflectiveInvocationContext invocationContext, + ExtensionContext extensionContext) throws Throwable { + invoke(extensionContext, invocation); + } + + @Override + public void interceptDynamicTest(Invocation invocation, + DynamicTestInvocationContext invocationContext, + ExtensionContext extensionContext) throws Throwable { + invoke(extensionContext, invocation); + } + + @Override + public void interceptAfterEachMethod(Invocation invocation, + ReflectiveInvocationContext invocationContext, + ExtensionContext extensionContext) throws Throwable { + invoke(extensionContext, invocation); + } + + @Override + public void interceptAfterAllMethod(Invocation invocation, + ReflectiveInvocationContext invocationContext, + ExtensionContext extensionContext) throws Throwable { + invoke(extensionContext, invocation); + } + + /** + * Service registry associated with the provided extension contexts + * (uses {@link #extensionStore(org.junit.jupiter.api.extension.ExtensionContext)}). + * + * @param extensionContext extension context + * @return service registry + */ + protected Optional registry(ExtensionContext extensionContext) { + return Optional.ofNullable(extensionStore(extensionContext) + .get(ServiceRegistry.class, ServiceRegistry.class)); + } + + /** + * Extension store used by this extension to store context, service registry etc. + * + * @param ctx extension context + * @return extension store + */ + protected ExtensionContext.Store extensionStore(ExtensionContext ctx) { + Class testClass = ctx.getRequiredTestClass(); + return ctx.getStore(ExtensionContext.Namespace.create(testClass)); + } + + /** + * 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 JUnit extension context + * @param context Helidon context to set + */ + 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(ContextSingleton.STATIC_CONTEXT_CLASSIFIER, context); + extensionStore(ctx) + .put(Context.class, context); + } + + /** + * The current 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)); + } + + /** + * Call a callable within context. + * + * @param ctx JUnit extension context + * @param callable callable to invoke + * @param type of the result + * @return result of the callable + * @throws Throwable in case the call to callable threw an exception + */ + protected T callInContext(ExtensionContext ctx, Callable callable) throws Throwable { + AtomicReference thrown = new AtomicReference<>(); + + T response = Contexts.runInContext(context(ctx).orElseThrow(), () -> { + try { + return callable.call(); + } catch (Throwable e) { + thrown.set(e); + return null; + } + }); + if (thrown.get() != null) { + throw thrown.get(); + } + return response; + } + + /** + * Call a callable that can throw {@link java.lang.Throwable} within context. + * + * @param ctx JUnit extension context + * @param callable callable to invoke + * @param type of the result + * @return result of the callable + * @throws Throwable in case the call to callable threw an exception + */ + protected T callWithThrowableInContext(ExtensionContext ctx, CallWithThrowable callable) throws Throwable { + AtomicReference thrown = new AtomicReference<>(); + + T response = Contexts.runInContext(context(ctx).orElseThrow(), () -> { + try { + return callable.call(); + } catch (Throwable e) { + thrown.set(e); + return null; + } + }); + if (thrown.get() != null) { + throw thrown.get(); + } + return response; + } + + /** + * Invoke a runnable that may throw a checked exception. + * + * @param ctx JUnit extension context + * @param runnable runnable to run + * @throws Throwable in case the runnable threw an exception + */ + protected void runInContext(ExtensionContext ctx, RunWithThrowable runnable) throws Throwable { + AtomicReference thrown = new AtomicReference<>(); + + Contexts.runInContext(context(ctx).orElseThrow(), () -> { + try { + runnable.run(); + } catch (Throwable e) { + thrown.set(e); + } + }); + if (thrown.get() != null) { + throw thrown.get(); + } + } + + /** + * Invoke a supplier within the context. + * + * @param ctx JUnit extension context + * @param supplier supplier to invoke + * @param type of the result + * @return result of the supplier + */ + protected T call(ExtensionContext ctx, Supplier supplier) { + AtomicReference thrown = new AtomicReference<>(); + + T response = Contexts.runInContext(context(ctx).orElseThrow(), () -> { + try { + return supplier.get(); + } catch (RuntimeException e) { + thrown.set(e); + return null; + } + }); + if (thrown.get() != null) { + throw thrown.get(); + } + return response; + } + + /** + * Run a runnable within context. + * + * @param ctx JUnit extension context + * @param runnable runnable to run + */ + protected void run(ExtensionContext ctx, Runnable runnable) { + AtomicReference thrown = new AtomicReference<>(); + + Contexts.runInContext(context(ctx).orElseThrow(), () -> { + try { + runnable.run(); + } catch (RuntimeException e) { + thrown.set(e); + } + }); + if (thrown.get() != null) { + throw thrown.get(); + } + } + + /** + * Invoke a JUnit invocation within context. + * + * @param ctx JUnit extension context + * @param invocation invocation to invoke + * @param type of the returned value + * @return result of the invocation + * @throws Throwable in case the invocation threw an exception + */ + protected T invoke(ExtensionContext ctx, Invocation invocation) throws Throwable { + AtomicReference thrown = new AtomicReference<>(); + + T response = Contexts.runInContext(context(ctx).orElseThrow(), () -> { + try { + return invocation.proceed(); + } catch (Throwable e) { + thrown.set(e); + return null; + } + }); + if (thrown.get() != null) { + throw thrown.get(); + } + return response; + } + + private void afterShutdownMethods(Class requiredTestClass) { + for (Method declaredMethod : requiredTestClass.getDeclaredMethods()) { + TestRegistry.AfterShutdown 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); + + } + } + } + } + + private void createRegistry(ExtensionContext.Store store, Class testClass) { + var registryConfig = ServiceRegistryConfig.builder(); + ConfigRegistrySupport.setUp(registryConfig, testClass); + + var manager = ServiceRegistryManager.create(registryConfig.build()); + var registry = manager.registry(); + GlobalServiceRegistry.registry(registry); + GlobalConfig.config(() -> registry.get(Config.class), true); + store.put(ServiceRegistry.class, registry); + store.put(ServiceRegistryManager.class, new ClosableRegistryManager(manager)); + } + + private boolean registrySupportedType(ExtensionContext ctx, 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); + } + + /** + * Runnable that may throw a checked exception. + */ + @FunctionalInterface + protected interface RunWithThrowable { + /** + * Run the task. + * + * @throws Throwable possible checked exception + */ + void run() throws Throwable; + } + + /** + * Runnable that may throw a checked exception. + * + * @param type of the returned value + */ + @FunctionalInterface + protected interface CallWithThrowable { + /** + * Run the task. + * + * @return the returned value + * @throws Throwable possible checked exception + */ + T call() throws Throwable; + } + + private record ClosableRegistryManager(ServiceRegistryManager manager) + implements ExtensionContext.Store.CloseableResource { + + @Override + public void close() throws Throwable { + manager.shutdown(); + } + } +} diff --git a/testing/junit5/src/main/java/io/helidon/testing/junit5/Testing.java b/testing/junit5/src/main/java/io/helidon/testing/junit5/Testing.java new file mode 100644 index 00000000000..16bdbcfceb4 --- /dev/null +++ b/testing/junit5/src/main/java/io/helidon/testing/junit5/Testing.java @@ -0,0 +1,43 @@ +/* + * 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.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 org.junit.jupiter.api.extension.ExtendWith; + +/** + * Helidon testing related annotations and APIs. + */ +public final class Testing { + private Testing() { + } + + /** + * A test class annotation that ensures Helidon extension is loaded. + */ + @Target({ElementType.ANNOTATION_TYPE, ElementType.TYPE}) + @Retention(RetentionPolicy.RUNTIME) + @Inherited + @ExtendWith(TestJunitExtension.class) + public @interface Test { + } +} diff --git a/testing/junit5/src/main/java/io/helidon/testing/junit5/package-info.java b/testing/junit5/src/main/java/io/helidon/testing/junit5/package-info.java new file mode 100644 index 00000000000..af5ac07b2c3 --- /dev/null +++ b/testing/junit5/src/main/java/io/helidon/testing/junit5/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ + +/** + * JUnit5 extensions for Helidon supporting configuration, and optionally the Inject service registry. + */ +package io.helidon.testing.junit5; diff --git a/testing/junit5/src/main/java/module-info.java b/testing/junit5/src/main/java/module-info.java new file mode 100644 index 00000000000..06eedab35d6 --- /dev/null +++ b/testing/junit5/src/main/java/module-info.java @@ -0,0 +1,28 @@ +/* + * 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. + */ + +/** + * See {@link io.helidon.testing.junit5}. + */ +module io.helidon.testing.junit5 { + requires org.junit.jupiter.api; + requires io.helidon.service.registry; + requires io.helidon.logging.common; + requires transitive io.helidon.testing; + requires transitive io.helidon.common.context; + + exports io.helidon.testing.junit5; +} \ No newline at end of file diff --git a/testing/pom.xml b/testing/pom.xml new file mode 100644 index 00000000000..2ce171a7251 --- /dev/null +++ b/testing/pom.xml @@ -0,0 +1,64 @@ + + + + + 4.0.0 + + io.helidon + helidon-project + 4.1.0-SNAPSHOT + + pom + io.helidon.testing + helidon-testing-project + Helidon Testing Project + + + true + + + + testing + junit5 + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + check-dependencies + verify + + + + + + + + + tests + + tests + + + + diff --git a/testing/testing/README.md b/testing/testing/README.md new file mode 100644 index 00000000000..289c09fe185 --- /dev/null +++ b/testing/testing/README.md @@ -0,0 +1,32 @@ +Helidon Testing +----- + +This module provides features that are most commonly required to test a Helidon application. + +# TestConfigSource + +Testing may require setting configuration values after the config instance is created, but before the value +is used. This can be achieved through `TestConfigSource` (when creating configuration instance by hand or via +ServiceRegistry; note that when using registry, there is nothing required to be done by you). + +The `TestConfig` static API can be used to set values, such as when using TestContainers. +You can also configure values directly on the `TestConfigSource`, if you own the instance (when creating config +by hand), or an instance looked up from the service registry. + +# Configuration annotations + +Configuration can be further customized by using configuration annotations. + +The following annotations are supported, to provide customized configuration. + +The following table shows all configuration types and their weight used when constructing config (if relevant): + +| Source | Weight | Description | +|-----------------------|-----------|----------------------------------------------------------------------------------------------------------------------------------| +| `TestConfigSource` | 1_000_000 | Use `TestConfig` to set values, always the highest weight form this table | +| `@TestConfig.Profile` | N/A | Customization of the config profile to use. Defaults to `test` | +| `@TestConfig.Value` | 954_000 | Used to set a single key/value pair. Weight can be customized. Repeatable. | +| `@TestConfig.Values` | 953_000 | Use to set multiple key/value pairs, with a format defined (such as yaml, properties). Weight can be customized. | +| `@TestConfig.File` | 952_000 | Use to set a whole config file. Type is determined based on file suffix. Weight can be customized. Repeatable. | +| `@TestConfig.Source` | 951_000 | Use to set a single config source. Weight can be customized. This annotation belongs to static method producing a config source. | + diff --git a/testing/testing/pom.xml b/testing/testing/pom.xml new file mode 100644 index 00000000000..c35d9acdd27 --- /dev/null +++ b/testing/testing/pom.xml @@ -0,0 +1,106 @@ + + + + + 4.0.0 + + io.helidon.testing + helidon-testing-project + 4.1.0-SNAPSHOT + ../pom.xml + + helidon-testing + Helidon Testing + + + + io.helidon.common + helidon-common + + + io.helidon.common + helidon-common-types + + + io.helidon.common + helidon-common-media-type + + + io.helidon.common + helidon-common-context + + + io.helidon.config + helidon-config + provided + + + io.helidon.service + helidon-service-registry + true + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.service + helidon-service-codegen + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-helidon-copyright + ${helidon.version} + + + + + + io.helidon.codegen + helidon-codegen-apt + ${helidon.version} + + + io.helidon.service + helidon-service-codegen + ${helidon.version} + + + io.helidon.codegen + helidon-codegen-helidon-copyright + ${helidon.version} + + + + + + diff --git a/testing/testing/src/main/java/io/helidon/testing/ConfigRegistrySupport.java b/testing/testing/src/main/java/io/helidon/testing/ConfigRegistrySupport.java new file mode 100644 index 00000000000..e7878ad29b8 --- /dev/null +++ b/testing/testing/src/main/java/io/helidon/testing/ConfigRegistrySupport.java @@ -0,0 +1,122 @@ +/* + * 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.testing; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Modifier; +import java.util.Set; +import java.util.stream.Stream; + +import io.helidon.common.media.type.MediaTypes; +import io.helidon.config.ConfigSources; +import io.helidon.config.spi.ConfigSource; +import io.helidon.service.registry.ExistingInstanceDescriptor; +import io.helidon.service.registry.ServiceRegistryConfig; + +/** + * Used by Helidon test extension to set up configuration with service registry. + */ +public final class ConfigRegistrySupport { + private ConfigRegistrySupport() { + } + + /** + * Set up the service registry with configuration from the test class' annotations. + * + * @param serviceRegistryConfig configuration of registry + * @param testClass test class to process + */ + public static void setUp(ServiceRegistryConfig.Builder serviceRegistryConfig, Class testClass) { + SetUpUtil.profile(testClass); + + SetUpUtil.value(testClass) + .forEach(it -> { + ConfigSource source = SetUpUtil.createValueSource(it); + serviceRegistryConfig.addServiceDescriptor(ExistingInstanceDescriptor.create(source, + Set.of(ConfigSource.class), + it.weight())); + }); + values(serviceRegistryConfig, testClass); + SetUpUtil.file(testClass) + .forEach(it -> { + SetUpUtil.createFileSource(it) + .forEach(source -> { + var desc = ExistingInstanceDescriptor.create(source, + Set.of(ConfigSource.class), + it.weight()); + serviceRegistryConfig.addServiceDescriptor(desc); + }); + + }); + sourceMethods(serviceRegistryConfig, testClass); + } + + private static void sourceMethods(ServiceRegistryConfig.Builder serviceRegistryConfig, Class testClass) { + Stream.of(testClass.getDeclaredMethods()) + .filter(it -> it.getAnnotation(TestConfig.Source.class) != null) + .forEach(method -> { + TestConfig.Source annotation = method.getAnnotation(TestConfig.Source.class); + // non-private static method, return type must be ConfigSource + int modifiers = method.getModifiers(); + if (!Modifier.isStatic(modifiers)) { + throw new TestException("Method " + method.getName() + " is annotated with " + + TestConfig.Source.class.getName() + " but it is not static."); + } + if (Modifier.isPrivate(modifiers)) { + throw new TestException("Method " + method.getName() + " is annotated with " + + TestConfig.Source.class.getName() + " but it is private."); + } + if (!ConfigSource.class.isAssignableFrom(method.getReturnType())) { + throw new TestException("Method " + method.getName() + " is annotated with " + + TestConfig.Source.class.getName() + " but it does not return " + + ConfigSource.class.getName()); + } + if (method.getParameterCount() != 0) { + throw new TestException("Method " + method.getName() + " is annotated with " + + TestConfig.Source.class.getName() + " but it has parameters"); + } + + // now we have a candidate that is good + try { + method.setAccessible(true); + ConfigSource source = (ConfigSource) method.invoke(null); + serviceRegistryConfig.addServiceDescriptor(ExistingInstanceDescriptor.create(source, + Set.of(ConfigSource.class), + annotation.weight())); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new TestException("Failed to invoke " + method.getName() + " method to obtain a config source"); + } + }); + } + + private static void values(ServiceRegistryConfig.Builder serviceRegistryConfig, Class testClass) { + TestConfig.Block annotation = testClass.getAnnotation(TestConfig.Block.class); + if (annotation == null) { + return; + } + var mediaType = MediaTypes.detectExtensionType(annotation.type()); + if (mediaType.isEmpty()) { + throw new TestException("No extension media type found for extension " + annotation.type() + ", for " + + "annotation " + annotation.annotationType().getName()); + } + var source = ConfigSources.create(annotation.value(), mediaType.get()); + serviceRegistryConfig.addServiceDescriptor(ExistingInstanceDescriptor.create(source, + Set.of(ConfigSource.class), + annotation.weight())); + } + +} diff --git a/testing/testing/src/main/java/io/helidon/testing/SetUpUtil.java b/testing/testing/src/main/java/io/helidon/testing/SetUpUtil.java new file mode 100644 index 00000000000..6df29f1a2d8 --- /dev/null +++ b/testing/testing/src/main/java/io/helidon/testing/SetUpUtil.java @@ -0,0 +1,82 @@ +/* + * 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.testing; + +import java.util.List; +import java.util.Map; + +import io.helidon.config.ConfigSources; +import io.helidon.config.spi.ConfigSource; + +final class SetUpUtil { + private SetUpUtil() { + } + + static List file(Class testClass) { + TestConfig.Files annotation = testClass.getAnnotation(TestConfig.Files.class); + if (annotation == null) { + TestConfig.File file = testClass.getAnnotation(TestConfig.File.class); + if (file == null) { + return List.of(); + } + return List.of(file); + } + return List.of(annotation.value()); + } + + static List value(Class testClass) { + TestConfig.Values annotation = testClass.getAnnotation(TestConfig.Values.class); + if (annotation == null) { + TestConfig.Value value = testClass.getAnnotation(TestConfig.Value.class); + if (value == null) { + return List.of(); + } + return List.of(value); + } + return List.of(annotation.value()); + } + + static void profile(Class testClass) { + TestConfig.Profile annotation = testClass.getAnnotation(TestConfig.Profile.class); + String profile = "test"; + + boolean force = false; + if (annotation != null) { + profile = annotation.value(); + force = true; + } + + boolean profileNotConfigured = System.getProperty("config.profile") == null + && System.getProperty("helidon.config.profile") == null + && System.getenv("HELIDON_CONFIG_PROFILE") == null; + + if (force || profileNotConfigured) { + System.setProperty("helidon.config.profile", profile); + } + } + + static ConfigSource createValueSource(TestConfig.Value value) { + return ConfigSources.create(Map.of(value.key(), value.value())) + .name("From " + value.getClass().getName()) + .build(); + } + + static List createFileSource(TestConfig.File value) { + return List.of(ConfigSources.file(value.value()).optional().build(), + ConfigSources.classpath(value.value()).optional().build()); + } +} diff --git a/testing/testing/src/main/java/io/helidon/testing/TestConfig.java b/testing/testing/src/main/java/io/helidon/testing/TestConfig.java new file mode 100644 index 00000000000..fb3a69560be --- /dev/null +++ b/testing/testing/src/main/java/io/helidon/testing/TestConfig.java @@ -0,0 +1,237 @@ +/* + * 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.testing; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.locks.ReentrantLock; + +import io.helidon.common.context.ContextSingleton; + +/** + * Static accessor to {@link io.helidon.testing.TestConfigSource} to be able to set values. + */ +public final class TestConfig { + private static final ContextSingleton SOURCES = ContextSingleton.create(TestConfig.class, + Sources.class, + Sources::new); + private static final ContextSingleton ACCUMULATED_OPTIONS = ContextSingleton.create( + TestConfig.class, + AccumulatedOptions.class, + AccumulatedOptions::new); + private static final ReentrantLock LOCK = new ReentrantLock(); + + private TestConfig() { + } + + /** + * Set a configuration key and value. + * This method CANNOT override an existing key, as such keys are already in the config snapshot. + *

+ * If you need to override a key that exists in existing production configuration, + * create a file that declares it in test resources with a reference to an unused key, such as {@code ${test.container.port}}, + * and then use this key to set the value at test time, usually in BeforeAll section of your test. + * + * @param key key to set + * @param value value to set + */ + public static void set(String key, String value) { + LOCK.lock(); + try { + if (SOURCES.isPresent()) { + SOURCES.get() + .sources() + .forEach(it -> it.set(key, value)); + } else { + ACCUMULATED_OPTIONS.get().options().put(key, value); + } + } finally { + LOCK.unlock(); + } + } + + static void register(TestConfigSource source) { + LOCK.lock(); + try { + if (!SOURCES.isPresent()) { + ACCUMULATED_OPTIONS.get().options().forEach(source::set); + ACCUMULATED_OPTIONS.get().options.clear(); + } + SOURCES.get().sources().add(source); + } finally { + LOCK.unlock(); + } + } + + /** + * Config profile to use. + */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + public @interface Profile { + /** + * Config profile to set. This will always override profile set by other means. + * + * @return config profile to use + */ + String value(); + } + + /** + * Custom configuration key/value pair. + */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + @Repeatable(Values.class) + public @interface Value { + /** + * Key of the added configuration option. + * + * @return key + */ + String key(); + + /** + * Value of the added configuration option. + * + * @return value + */ + String value(); + + /** + * Weight of this configuration option. + * + * @return weight + */ + double weight() default 954_000; + } + + /** + * Repeatable annotation for {@link io.helidon.testing.TestConfig.Value}. + */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + public @interface Values { + /** + * Config values. + * + * @return values + */ + Value[] value(); + } + + /** + * Defines the configuration as a String in {@link #value()} for the + * given {@link #type()}. + */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + public @interface Block { + /** + * Text block with configuration data. + * + * @return configuration data + */ + String value(); + + /** + * Type of the value (file suffix). + * + * @return type to use, defaults to {@code properties} + */ + String type() default "properties"; + + /** + * Weight of this configuration option. + * + * @return weight + */ + double weight() default 953_000; + } + + /** + * Custom configuration file (classpath or file system, first looks for file system, second for file). + */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + @Repeatable(Files.class) + public @interface File { + /** + * File path, relative to the module (either classpath, or file system path). + * + * @return path + */ + String value(); + + /** + * Weight of this configuration option. + * + * @return weight + */ + double weight() default 952_000; + } + + /** + * Repeatable annotation for {@link io.helidon.testing.TestConfig.File}. + */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + public @interface Files { + /** + * Config values. + * + * @return values + */ + File[] value(); + } + + /** + * Custom configuration source. + *

+ * This annotation annotates a static method producing a {@link io.helidon.config.spi.ConfigSource}, or a list of + * them. + */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + public @interface Source { + /** + * Weight of this configuration option. + * + * @return weight + */ + double weight() default 951_000; + } + + private record Sources(List sources) { + Sources() { + this(new ArrayList<>()); + } + } + + private record AccumulatedOptions(Map options) { + private AccumulatedOptions() { + this(new HashMap<>()); + } + } +} diff --git a/testing/testing/src/main/java/io/helidon/testing/TestConfigSource.java b/testing/testing/src/main/java/io/helidon/testing/TestConfigSource.java new file mode 100644 index 00000000000..5c5fd2ec57a --- /dev/null +++ b/testing/testing/src/main/java/io/helidon/testing/TestConfigSource.java @@ -0,0 +1,105 @@ +/* + * 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.testing; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import io.helidon.common.Weight; +import io.helidon.config.spi.ConfigNode; +import io.helidon.config.spi.ConfigSource; +import io.helidon.config.spi.LazyConfigSource; +import io.helidon.service.registry.Service; + +/** + * Config source that is defined as a {@link io.helidon.config.spi.LazyConfigSource}, thus deferring lookup + * until the latest possible moment in time. + *

+ * This config source allows change in values after the config instance was created. + *

+ * Note that this can only work if the key is requested AFTER the value is set on this config source, as Helidon + * config does not support mutability (config is a snapshot except for {@link io.helidon.config.spi.LazyConfigSource}, which + * whose results are still cached forever in the snapshot). + *

+ * To use this config source: + *

    + *
  • When using Service registry: look up this instance from the registry and set the values, before they are used + * by tests. Note that for example ServerTest will configure the local server endpoint under {@code test.server.port}
  • + *
  • When using manual configuration setup, add a new instance to the config builder.
  • + *
+ * + * @see io.helidon.testing.TestConfig + */ +@Service.Provider +@Weight(1_000_000) +public class TestConfigSource implements ConfigSource, LazyConfigSource { + private final Map options = new HashMap<>(); + private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); + + TestConfigSource() { + TestConfig.register(this); + } + + /** + * Get a new instance without any configured keys. + * + * @return instance of a test config source. + */ + public static TestConfigSource create() { + return new TestConfigSource(); + } + + @Override + public Optional node(String key) { + lock.readLock().lock(); + try { + return Optional.ofNullable(options.get(key)) + .map(ConfigNode.ValueNode::create); + } finally { + lock.readLock().unlock(); + } + } + + /** + * Set a value. + * + * @param key key to set + * @param value value to set + */ + public void set(String key, String value) { + lock.writeLock().lock(); + try { + options.put(key, value); + } finally { + lock.writeLock().unlock(); + } + } + + /** + * Clear all values. + */ + public void clear() { + lock.writeLock().lock(); + try { + options.clear(); + } finally { + lock.writeLock().unlock(); + } + } +} diff --git a/testing/testing/src/main/java/io/helidon/testing/TestException.java b/testing/testing/src/main/java/io/helidon/testing/TestException.java new file mode 100644 index 00000000000..1c67f9f1189 --- /dev/null +++ b/testing/testing/src/main/java/io/helidon/testing/TestException.java @@ -0,0 +1,43 @@ +/* + * 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.testing; + +import java.util.Objects; + +/** + * Exception during Helidon test processing. + */ +public class TestException extends RuntimeException { + /** + * Create a new exception with a customized message. + * + * @param message message + */ + public TestException(String message) { + super(Objects.requireNonNull(message)); + } + + /** + * Create a new exception with a customized message and a cause. + * + * @param message message + * @param cause cause of this exception + */ + public TestException(String message, Throwable cause) { + super(Objects.requireNonNull(message), Objects.requireNonNull(cause)); + } +} diff --git a/testing/testing/src/main/java/io/helidon/testing/TestRegistry.java b/testing/testing/src/main/java/io/helidon/testing/TestRegistry.java new file mode 100644 index 00000000000..f804b0cf2fb --- /dev/null +++ b/testing/testing/src/main/java/io/helidon/testing/TestRegistry.java @@ -0,0 +1,43 @@ +/* + * 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.testing; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Container for registry testing annotations. + */ +public final class TestRegistry { + private TestRegistry() { + } + + /** + * Mark a static method to be executed after the service registry has been shut-down. + *

+ * This will only be executed after all pre-destroy methods have been called and the registry is no + * longer available (i.e. you cannot obtain the registry instance that was used during all of your test + * methods). + */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + public @interface AfterShutdown { + } + +} diff --git a/testing/testing/src/main/java/io/helidon/testing/package-info.java b/testing/testing/src/main/java/io/helidon/testing/package-info.java new file mode 100644 index 00000000000..6925e8d2512 --- /dev/null +++ b/testing/testing/src/main/java/io/helidon/testing/package-info.java @@ -0,0 +1,26 @@ +/* + * 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. + */ + +/** + * Testing annotations and extension for Helidon applications. + * This module provides support for + *

    + *
  • Configuration setup through {@link io.helidon.testing.TestConfigSource}
  • + *
  • Statically set configuration using {@link io.helidon.testing.TestConfig}
  • + *
  • Annotation support - inner classes of {@link io.helidon.testing.TestConfig}
  • + *
+ */ +package io.helidon.testing; diff --git a/testing/testing/src/main/java/module-info.java b/testing/testing/src/main/java/module-info.java new file mode 100644 index 00000000000..ee43dda9030 --- /dev/null +++ b/testing/testing/src/main/java/module-info.java @@ -0,0 +1,32 @@ +/* + * 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. + */ + +/** + * Testing annotations and extension for Helidon applications. + * This module provides support for + *
    + *
  • Configuration setup through {@link io.helidon.testing.TestConfigSource}
  • + *
+ */ +module io.helidon.testing { + requires static io.helidon.service.registry; + + requires io.helidon.common; + requires io.helidon.common.context; + requires io.helidon.config; + + exports io.helidon.testing; +} \ No newline at end of file diff --git a/testing/tests/junit5/pom.xml b/testing/tests/junit5/pom.xml new file mode 100644 index 00000000000..926a1dad8e6 --- /dev/null +++ b/testing/tests/junit5/pom.xml @@ -0,0 +1,60 @@ + + + + + io.helidon.testing.tests + helidon-testing-tests-project + 4.1.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + helidon-testing-tests-junit5 + + Helidon Testing Tests JUnit5 + + + + io.helidon.config + helidon-config + + + io.helidon.testing + helidon-testing-junit5 + test + + + io.helidon.common.testing + helidon-common-testing-junit5 + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + diff --git a/testing/tests/junit5/src/test/java/io/helidon/testing/tests/junit5/Junit5Test.java b/testing/tests/junit5/src/test/java/io/helidon/testing/tests/junit5/Junit5Test.java new file mode 100644 index 00000000000..195f991a5c8 --- /dev/null +++ b/testing/tests/junit5/src/test/java/io/helidon/testing/tests/junit5/Junit5Test.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2019, 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.testing.tests.junit5; + +import java.util.Map; + +import io.helidon.common.config.Config; +import io.helidon.common.config.GlobalConfig; +import io.helidon.config.ConfigSources; +import io.helidon.config.spi.ConfigSource; +import io.helidon.testing.TestConfig; +import io.helidon.testing.junit5.Testing; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +/* +This test validates that all possible configuration options are correctly loaded. +It also validates that weight of each annotation is handled correctly + */ +@TestConfig.Value(key = "test.first", value = "first") +@TestConfig.Value(key = "test.second", value = "second") +@TestConfig.Block(""" + test.first=wrong + test.third=third + test.fourth=fourth + """) +@TestConfig.File("test-config.properties") +@Testing.Test +public class Junit5Test { + private static Config config; + + @TestConfig.Source + public static ConfigSource customSourceSix() { + return ConfigSources.create(Map.of("test.fifth", "wrong", + "test.sixth", "sixth")).build(); + } + + @TestConfig.Source + public static ConfigSource customSourceSeven() { + return ConfigSources.create(Map.of("test.seventh", "seventh")).build(); + } + + @BeforeAll + public static void beforeAll() { + TestConfig.set("test.eighth", "eighth"); + config = GlobalConfig.config(); + } + + @Test + public void testValueConfig() { + assertThat(config.get("test.first").asString().get(), is("first")); + assertThat(config.get("test.second").asString().get(), is("second")); + } + + @Test + public void testValuesConfig() { + assertThat(config.get("test.third").asString().get(), is("third")); + assertThat(config.get("test.fourth").asString().get(), is("fourth")); + } + + @Test + public void testFileConfig() { + assertThat(config.get("test.fifth").asString().get(), is("fifth")); + } + + @Test + public void testSourceConfigSix() { + assertThat(config.get("test.sixth").asString().get(), is("sixth")); + } + + @Test + public void testSourceConfigSeven() { + assertThat(config.get("test.seventh").asString().get(), is("seventh")); + } + + @Test + public void testStaticConfig() { + assertThat(config.get("test.eighth").asString().get(), is("eighth")); + } +} diff --git a/testing/tests/junit5/src/test/resources/test-config.properties b/testing/tests/junit5/src/test/resources/test-config.properties new file mode 100644 index 00000000000..ea3ffc049b2 --- /dev/null +++ b/testing/tests/junit5/src/test/resources/test-config.properties @@ -0,0 +1,18 @@ +# +# 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. +# + +test.third=wrong +test.fifth=fifth diff --git a/testing/tests/pom.xml b/testing/tests/pom.xml new file mode 100644 index 00000000000..30a9055a064 --- /dev/null +++ b/testing/tests/pom.xml @@ -0,0 +1,49 @@ + + + + + io.helidon.testing + helidon-testing-project + 4.1.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + io.helidon.testing.tests + helidon-testing-tests-project + + Helidon Testing Tests Project + pom + + + true + true + true + true + true + true + true + + + + junit5 + + diff --git a/tracing/providers/opentelemetry/src/main/java/io/helidon/tracing/providers/opentelemetry/OpenTelemetryTracerProvider.java b/tracing/providers/opentelemetry/src/main/java/io/helidon/tracing/providers/opentelemetry/OpenTelemetryTracerProvider.java index 67b4dfb1eb0..ca8d7411312 100644 --- a/tracing/providers/opentelemetry/src/main/java/io/helidon/tracing/providers/opentelemetry/OpenTelemetryTracerProvider.java +++ b/tracing/providers/opentelemetry/src/main/java/io/helidon/tracing/providers/opentelemetry/OpenTelemetryTracerProvider.java @@ -20,10 +20,9 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; -import io.helidon.common.LazyValue; import io.helidon.common.Weight; import io.helidon.common.Weighted; -import io.helidon.common.context.Context; +import io.helidon.common.context.ContextSingleton; import io.helidon.common.context.Contexts; import io.helidon.tracing.Span; import io.helidon.tracing.Tracer; @@ -41,18 +40,22 @@ public class OpenTelemetryTracerProvider implements TracerProvider { private static final System.Logger LOGGER = System.getLogger(OpenTelemetryTracerProvider.class.getName()); private static final AtomicReference CONFIGURED_TRACER = new AtomicReference<>(); private static final AtomicBoolean GLOBAL_SET = new AtomicBoolean(); - private static final LazyValue GLOBAL_TRACER; + private static final ContextSingleton GLOBAL_TRACER; static { - GLOBAL_TRACER = LazyValue.create(() -> { + GLOBAL_TRACER = ContextSingleton.create(OpenTelemetryTracerProvider.class, + Tracer.class, () -> { // try to get from configured global tracer Tracer tracer = CONFIGURED_TRACER.get(); if (tracer != null) { return tracer; } - Context global = Contexts.globalContext(); - return global.get(OpenTelemetryTracer.class) + ContextSingleton otelTracer = ContextSingleton.create(OpenTelemetryTracerProvider.class, + OpenTelemetryTracer.class); + + return otelTracer.value() .map(Tracer.class::cast) + .or(() -> Contexts.globalContext().get(OpenTelemetryTracer.class)) .orElseGet(() -> { if (LOGGER.isLoggable(System.Logger.Level.TRACE)) { LOGGER.log(System.Logger.Level.TRACE, "Global tracer is not registered. Register it through " diff --git a/webserver/observe/metrics/src/main/java/io/helidon/webserver/observe/metrics/KeyPerformanceIndicatorMetricsImpls.java b/webserver/observe/metrics/src/main/java/io/helidon/webserver/observe/metrics/KeyPerformanceIndicatorMetricsImpls.java index 830305cd2e4..7b598d99e0d 100644 --- a/webserver/observe/metrics/src/main/java/io/helidon/webserver/observe/metrics/KeyPerformanceIndicatorMetricsImpls.java +++ b/webserver/observe/metrics/src/main/java/io/helidon/webserver/observe/metrics/KeyPerformanceIndicatorMetricsImpls.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2023 Oracle and/or its affiliates. + * Copyright (c) 2021, 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. @@ -22,6 +22,7 @@ import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; +import io.helidon.common.context.ContextSingleton; import io.helidon.metrics.api.Counter; import io.helidon.metrics.api.Gauge; import io.helidon.metrics.api.KeyPerformanceIndicatorMetricsConfig; @@ -53,7 +54,10 @@ class KeyPerformanceIndicatorMetricsImpls { static final String LOAD_NAME = "load"; static final String KPI_METERS_SCOPE = Meter.Scope.VENDOR; - private static final Map KPI_METRICS = new HashMap<>(); + private static final ContextSingleton KPI_METRICS = + ContextSingleton.create(KeyPerformanceIndicatorMetricsImpls.class, + KpiMetrics.class, + KpiMetrics::new); private KeyPerformanceIndicatorMetricsImpls() { } @@ -69,14 +73,16 @@ private KeyPerformanceIndicatorMetricsImpls() { static KeyPerformanceIndicatorSupport.Metrics get(MeterRegistry kpiMeterRegistry, String meterNamePrefix, KeyPerformanceIndicatorMetricsConfig kpiConfig) { - return KPI_METRICS.computeIfAbsent(meterNamePrefix, prefix -> - kpiConfig.extended() - ? new Extended(kpiMeterRegistry, meterNamePrefix, kpiConfig) - : new Basic(kpiMeterRegistry, meterNamePrefix)); + return KPI_METRICS.get() + .kpiMetrics + .computeIfAbsent(meterNamePrefix, prefix -> + kpiConfig.extended() + ? new Extended(kpiMeterRegistry, meterNamePrefix, kpiConfig) + : new Basic(kpiMeterRegistry, meterNamePrefix)); } static void close() { - KPI_METRICS.clear(); + KPI_METRICS.get().kpiMetrics.clear(); } /** @@ -105,7 +111,9 @@ public void onRequestReceived() { @Override public void close() { meters.forEach(meterRegistry::remove); - KPI_METRICS.clear(); + KPI_METRICS.get() + .kpiMetrics + .clear(); } protected M add(M meter) { @@ -221,4 +229,8 @@ void completeRequest() { } } } + + private static class KpiMetrics { + private final Map kpiMetrics = new HashMap<>(); + } } diff --git a/webserver/observe/metrics/src/main/java/io/helidon/webserver/observe/metrics/MetricsObserver.java b/webserver/observe/metrics/src/main/java/io/helidon/webserver/observe/metrics/MetricsObserver.java index 7d8df16015d..c006c949476 100644 --- a/webserver/observe/metrics/src/main/java/io/helidon/webserver/observe/metrics/MetricsObserver.java +++ b/webserver/observe/metrics/src/main/java/io/helidon/webserver/observe/metrics/MetricsObserver.java @@ -55,7 +55,7 @@ @RuntimeType.PrototypedBy(MetricsObserverConfig.class) public class MetricsObserver implements Observer, RuntimeType.Api { private final MetricsObserverConfig config; - private MetricsFeature metricsFeature; + private final MetricsFeature metricsFeature; private MetricsObserver(MetricsObserverConfig config) { this.config = config; diff --git a/webserver/observe/metrics/src/test/java/io/helidon/webserver/observe/metrics/TestMetricsConfigPropagation.java b/webserver/observe/metrics/src/test/java/io/helidon/webserver/observe/metrics/TestMetricsConfigPropagation.java index 0d19112d0d3..a223046905f 100644 --- a/webserver/observe/metrics/src/test/java/io/helidon/webserver/observe/metrics/TestMetricsConfigPropagation.java +++ b/webserver/observe/metrics/src/test/java/io/helidon/webserver/observe/metrics/TestMetricsConfigPropagation.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 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. @@ -16,9 +16,8 @@ package io.helidon.webserver.observe.metrics; import io.helidon.common.media.type.MediaTypes; +import io.helidon.webclient.api.ClientResponseTyped; import io.helidon.webclient.http1.Http1Client; -import io.helidon.webclient.http1.Http1ClientResponse; -import io.helidon.webserver.WebServer; import io.helidon.webserver.testing.junit5.ServerTest; import jakarta.json.JsonObject; @@ -35,27 +34,29 @@ class TestMetricsConfigPropagation { private final Http1Client client; - TestMetricsConfigPropagation(WebServer server, Http1Client client) { + TestMetricsConfigPropagation(Http1Client client) { this.client = client; } @Test - void checkExtendedKpiMetrics() { - try (Http1ClientResponse response = client.get("/observe/metrics") + void checkExtendedKpiMetrics() throws InterruptedException { + var response = client.get("/observe/metrics") .accept(MediaTypes.APPLICATION_JSON) - .request()) { - assertThat("Metrics endpoint", response.status().code(), is(200)); - JsonObject metricsResponse = response.as(JsonObject.class); - JsonObject vendorMeters = metricsResponse.getJsonObject("vendor"); - assertThat("Vendor meters", vendorMeters, notNullValue()); - - // Make sure that the extended KPI metrics were turned on as directed by the configuration. - assertThat("Metrics KPI load", - vendorMeters.getJsonNumber("requests.load").intValue(), - greaterThan(0)); - - // Make sure that requests.count is absent because of the filtering in the config. - assertThat("Metrics KPI requests.count", vendorMeters.get("requests.count"), nullValue()); - } + .request(JsonObject.class); + + JsonObject metricsResponse = response.entity(); + JsonObject vendorMeters = metricsResponse.getJsonObject("vendor"); + assertThat("Vendor meters", vendorMeters, notNullValue()); + + // Make sure that the extended KPI metrics were turned on as directed by the configuration. + assertThat("Metrics KPI load", + vendorMeters.getJsonNumber("requests.load"), + notNullValue()); + assertThat("Metrics KPI load", + vendorMeters.getJsonNumber("requests.load").intValue(), + greaterThan(0)); + + // Make sure that requests.count is absent because of the filtering in the config. + assertThat("Metrics KPI requests.count", vendorMeters.get("requests.count"), nullValue()); } } diff --git a/webserver/observe/metrics/src/test/resources/application.yaml b/webserver/observe/metrics/src/test/resources/application.yaml index 204c874771f..4e4e2cc8413 100644 --- a/webserver/observe/metrics/src/test/resources/application.yaml +++ b/webserver/observe/metrics/src/test/resources/application.yaml @@ -1,5 +1,5 @@ # -# Copyright (c) 2023 Oracle and/or its affiliates. +# Copyright (c) 2023, 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. @@ -22,6 +22,6 @@ server: scopes: - name: vendor filter: - include: requests.l.* + include: requests\.l.* key-performance-indicators: extended: true diff --git a/webserver/security/src/main/java/io/helidon/webserver/security/SecurityConfigSupport.java b/webserver/security/src/main/java/io/helidon/webserver/security/SecurityConfigSupport.java index d13faa09805..24737b1588f 100644 --- a/webserver/security/src/main/java/io/helidon/webserver/security/SecurityConfigSupport.java +++ b/webserver/security/src/main/java/io/helidon/webserver/security/SecurityConfigSupport.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 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. @@ -61,7 +61,9 @@ private void security(SecurityFeatureConfig.BuilderBase target) { if (security.isPresent()) { return; } - security = Contexts.globalContext().get(Security.class); + security = Contexts.context() + .orElseGet(Contexts::globalContext) + .get(Security.class); if (security.isPresent()) { target.security(security.get()); return; diff --git a/webserver/security/src/test/java/io/helidon/webserver/security/WebSecurityBuilderGateDefaultsTest.java b/webserver/security/src/test/java/io/helidon/webserver/security/WebSecurityBuilderGateDefaultsTest.java index a1e4d853187..1bb62368cf5 100644 --- a/webserver/security/src/test/java/io/helidon/webserver/security/WebSecurityBuilderGateDefaultsTest.java +++ b/webserver/security/src/test/java/io/helidon/webserver/security/WebSecurityBuilderGateDefaultsTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2023 Oracle and/or its affiliates. + * Copyright (c) 2018, 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. @@ -19,7 +19,6 @@ import java.util.Optional; import java.util.Set; -import io.helidon.common.context.Context; import io.helidon.common.context.Contexts; import io.helidon.config.Config; import io.helidon.config.ConfigSources; @@ -86,8 +85,9 @@ public static void setup(WebServerConfig.Builder serverBuilder) { .addAuditProvider(myAuditProvider) .build(); - Context context = Context.create(); - context.register(myAuditProvider); + Contexts.context() + .orElseGet(Contexts::globalContext) + .register(myAuditProvider); serverBuilder .featuresDiscoverServices(false) @@ -96,7 +96,6 @@ public static void setup(WebServerConfig.Builder serverBuilder) { .defaults(SecurityFeature.rolesAllowed("admin")) .build()) .addFeature(ContextFeature.create()) - .serverContext(context) .routing(builder -> builder // will only accept admin (due to gate defaults) .get("/noRoles", SecurityFeature.authenticate()) diff --git a/webserver/security/src/test/java/io/helidon/webserver/security/WebSecurityFromConfigTest.java b/webserver/security/src/test/java/io/helidon/webserver/security/WebSecurityFromConfigTest.java index 9b40a4681bc..2096cef53ac 100644 --- a/webserver/security/src/test/java/io/helidon/webserver/security/WebSecurityFromConfigTest.java +++ b/webserver/security/src/test/java/io/helidon/webserver/security/WebSecurityFromConfigTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2023 Oracle and/or its affiliates. + * Copyright (c) 2018, 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. @@ -49,15 +49,17 @@ public static void setup(WebServerConfig.Builder serverBuilder) { Security security = Security.builder() .config(config.get("security")) - .addAuditProvider(myAuditProvider).build(); - // needed for other features, such as integration with webserver - Contexts.globalContext().register(security); + .addAuditProvider(myAuditProvider) + .build(); - Context context = Context.create(); + // needed for other features, such as integration with webserver + Context context = Contexts.context() + .orElseGet(Contexts::globalContext); + context.register(security); context.register(myAuditProvider); + serverBuilder - .serverContext(context) .config(config.get("server")) .routing(routing -> routing.get("/*", (req, res) -> { Optional securityContext = Contexts.context() diff --git a/webserver/security/src/test/java/io/helidon/webserver/security/WebSecurityProgrammaticTest.java b/webserver/security/src/test/java/io/helidon/webserver/security/WebSecurityProgrammaticTest.java index 14fd8e1c3ec..42d9b4622ca 100644 --- a/webserver/security/src/test/java/io/helidon/webserver/security/WebSecurityProgrammaticTest.java +++ b/webserver/security/src/test/java/io/helidon/webserver/security/WebSecurityProgrammaticTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2023 Oracle and/or its affiliates. + * Copyright (c) 2018, 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. @@ -19,7 +19,6 @@ import java.util.Optional; import java.util.regex.Pattern; -import io.helidon.common.context.Context; import io.helidon.common.context.Contexts; import io.helidon.config.Config; import io.helidon.config.ConfigSources; @@ -55,11 +54,11 @@ public static void setup(WebServerConfig.Builder serverBuilder) { Security security = Security.builder(config.get("security")) .addAuditProvider(myAuditProvider).build(); - Context context = Context.create(); - context.register(myAuditProvider); + Contexts.context() + .orElseGet(Contexts::globalContext) + .register(myAuditProvider); - serverBuilder.serverContext(context) - .featuresDiscoverServices(false) + serverBuilder.featuresDiscoverServices(false) .addFeature(ContextFeature.create()) .addFeature(SecurityFeature.builder() .security(security) diff --git a/webserver/security/src/test/resources/security-application.yaml b/webserver/security/src/test/resources/security-application.yaml index 504929aa6fe..f2ae0014db3 100644 --- a/webserver/security/src/test/resources/security-application.yaml +++ b/webserver/security/src/test/resources/security-application.yaml @@ -1,5 +1,5 @@ # -# Copyright (c) 2016, 2023 Oracle and/or its affiliates. +# Copyright (c) 2016, 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. @@ -47,6 +47,7 @@ server: - path: "/auditOnly" # method - any # audit all methods (by default GET and HEAD are not audited) + methods: ["get"] audit: true audit-event-type: "unit_test" audit-message-format: "Unit test message format" diff --git a/webserver/testing/junit5/junit5/pom.xml b/webserver/testing/junit5/junit5/pom.xml index 53f4b20319d..4346bb1449c 100644 --- a/webserver/testing/junit5/junit5/pom.xml +++ b/webserver/testing/junit5/junit5/pom.xml @@ -42,6 +42,18 @@ io.helidon.webclient helidon-webclient
+ + io.helidon.service + helidon-service-registry + + + io.helidon.testing + helidon-testing + + + io.helidon.testing + helidon-testing-junit5 + io.helidon.common.testing helidon-common-testing-http-junit5 diff --git a/webserver/testing/junit5/junit5/src/main/java/io/helidon/webserver/testing/junit5/HelidonRoutingJunitExtension.java b/webserver/testing/junit5/junit5/src/main/java/io/helidon/webserver/testing/junit5/HelidonRoutingJunitExtension.java index 2939ad1f57f..c90d42efd03 100644 --- a/webserver/testing/junit5/junit5/src/main/java/io/helidon/webserver/testing/junit5/HelidonRoutingJunitExtension.java +++ b/webserver/testing/junit5/junit5/src/main/java/io/helidon/webserver/testing/junit5/HelidonRoutingJunitExtension.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2022, 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. @@ -67,6 +67,7 @@ class HelidonRoutingJunitExtension extends JunitExtensionBase public void beforeAll(ExtensionContext context) { LogConfig.configureRuntime(); + super.beforeAll(context); Class testClass = context.getRequiredTestClass(); super.testClass(testClass); RoutingTest testAnnot = testClass.getAnnotation(RoutingTest.class); @@ -114,7 +115,8 @@ public boolean supportsParameter(ParameterContext parameterContext, ExtensionCon } Class paramType = parameterContext.getParameter().getType(); - return Contexts.globalContext() + return Contexts.context() + .orElseGet(Contexts::globalContext) .get(paramType) .isPresent(); } @@ -131,7 +133,8 @@ public Object resolveParameter(ParameterContext parameterContext, ExtensionConte } } - return Contexts.globalContext() + return Contexts.context() + .orElseGet(Contexts::globalContext) .get(paramType) .orElseThrow(() -> new ParameterResolutionException("Failed to resolve parameter of type " + paramType.getName())); 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 68dcf78f356..d86d12f0bc0 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2022, 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. @@ -33,18 +33,19 @@ import io.helidon.common.config.GlobalConfig; import io.helidon.common.context.Context; import io.helidon.common.context.Contexts; -import io.helidon.logging.common.LogConfig; +import io.helidon.service.registry.GlobalServiceRegistry; +import io.helidon.testing.TestConfig; import io.helidon.webserver.ListenerConfig; import io.helidon.webserver.Router; import io.helidon.webserver.WebServer; import io.helidon.webserver.WebServerConfig; +import io.helidon.webserver.WebServerRegistryService; import io.helidon.webserver.testing.junit5.spi.ServerJunitExtension; import org.junit.jupiter.api.extension.AfterAllCallback; import org.junit.jupiter.api.extension.AfterEachCallback; import org.junit.jupiter.api.extension.BeforeAllCallback; import org.junit.jupiter.api.extension.ExtensionContext; -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; @@ -59,7 +60,6 @@ class HelidonServerJunitExtension extends JunitExtensionBase implements BeforeAllCallback, AfterAllCallback, AfterEachCallback, - InvocationInterceptor, ParameterResolver { private final Map uris = new ConcurrentHashMap<>(); @@ -73,39 +73,54 @@ class HelidonServerJunitExtension extends JunitExtensionBase @Override public void beforeAll(ExtensionContext context) { - LogConfig.configureRuntime(); + super.beforeAll(context); - Class testClass = context.getRequiredTestClass(); - super.testClass(testClass); - ServerTest testAnnot = testClass.getAnnotation(ServerTest.class); - if (testAnnot == null) { - throw new IllegalStateException("Invalid test class for this extension: " + testClass); - } + run(context, () -> { + if (System.getProperty("helidon.config.profile") == null + && System.getProperty("config.profile") == null) { + System.setProperty("helidon.config.profile", "test"); + } + + Class testClass = context.getRequiredTestClass(); + super.testClass(testClass); + ServerTest testAnnot = testClass.getAnnotation(ServerTest.class); + if (testAnnot == null) { + throw new IllegalStateException("Invalid test class for this extension: " + testClass); + } - WebServerConfig.Builder builder = WebServer.builder() - .config(GlobalConfig.config().get("server")) - .host("localhost"); + WebServerConfig.Builder builder = WebServer.builder(); - extensions.forEach(it -> it.beforeAll(context)); - extensions.forEach(it -> it.updateServerBuilder(builder)); + builder.config(GlobalConfig.config().get("server")) + .host("localhost"); - // port will be random - builder.port(0) - .shutdownHook(false); + registrySetup(builder); - setupServer(builder); - addRouting(builder); + extensions.forEach(it -> it.beforeAll(context)); + extensions.forEach(it -> it.updateServerBuilder(builder)); - server = builder.build().start(); - if (server.hasTls()) { - uris.put(DEFAULT_SOCKET_NAME, URI.create("https://localhost:" + server.port() + "/")); - } else { - uris.put(DEFAULT_SOCKET_NAME, URI.create("http://localhost:" + server.port() + "/")); - } + // port will be random + builder.port(0) + .shutdownHook(false); + + setupServer(builder); + addRouting(builder); + + server = builder + .serverContext(super.context(context).orElseThrow()) // created above when we call super.beforeAll + .build().start(); + if (server.hasTls()) { + uris.put(DEFAULT_SOCKET_NAME, URI.create("https://localhost:" + server.port() + "/")); + } else { + uris.put(DEFAULT_SOCKET_NAME, URI.create("http://localhost:" + server.port() + "/")); + } + + TestConfig.set("test.server.port", String.valueOf(server.port())); + }); } @Override public void afterAll(ExtensionContext extensionContext) { + run(extensionContext, () -> { extensions.forEach(it -> it.afterAll(extensionContext)); if (server != null) { @@ -113,68 +128,87 @@ public void afterAll(ExtensionContext extensionContext) { } super.afterAll(extensionContext); + }); } @Override public void afterEach(ExtensionContext extensionContext) { - extensions.forEach(it -> it.afterEach(extensionContext)); + run(extensionContext, () -> extensions.forEach(it -> it.afterEach(extensionContext))); } @Override public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { - Class paramType = parameterContext.getParameter().getType(); - if (paramType.equals(WebServer.class)) { - return true; - } - if (paramType.equals(URI.class)) { - return true; - } - - for (ServerJunitExtension extension : extensions) { - if (extension.supportsParameter(parameterContext, extensionContext)) { + return call(extensionContext, () -> { + Class paramType = parameterContext.getParameter().getType(); + if (paramType.equals(WebServer.class)) { + return true; + } + if (paramType.equals(URI.class)) { return true; } - } - Context context; - if (server == null) { - context = Contexts.globalContext(); - } else { - context = server.context(); - } - return context.get(paramType).isPresent(); + for (ServerJunitExtension extension : extensions) { + if (extension.supportsParameter(parameterContext, extensionContext)) { + return true; + } + } + + Context context; + if (server == null) { + context = Contexts.context().orElseGet(Contexts::globalContext); + } else { + context = server.context(); + } + if (context.get(paramType).isPresent()) { + return true; + } + return super.supportsParameter(parameterContext, extensionContext); + }); } @Override public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { - Class paramType = parameterContext.getParameter().getType(); - if (paramType.equals(WebServer.class)) { - return server; - } - if (paramType.equals(URI.class)) { - return uri(parameterContext.getDeclaringExecutable(), Junit5Util.socketName(parameterContext.getParameter())); - } + return call(extensionContext, () -> { + Class paramType = parameterContext.getParameter().getType(); + if (paramType.equals(WebServer.class)) { + return server; + } + if (paramType.equals(URI.class)) { + return uri(parameterContext.getDeclaringExecutable(), Junit5Util.socketName(parameterContext.getParameter())); + } + + for (ServerJunitExtension extension : extensions) { + if (extension.supportsParameter(parameterContext, extensionContext)) { + return extension.resolveParameter(parameterContext, extensionContext, paramType, server); + } + } - for (ServerJunitExtension extension : extensions) { - if (extension.supportsParameter(parameterContext, extensionContext)) { - return extension.resolveParameter(parameterContext, extensionContext, paramType, server); + Context context; + if (server == null) { + context = Contexts.context().orElseGet(Contexts::globalContext); + } else { + context = server.context(); } - } - Context context; - if (server == null) { - context = Contexts.globalContext(); - } else { - context = server.context(); - } + var fromContext = context.get(paramType); + + if (fromContext.isPresent()) { + return fromContext; + } + + return super.resolveParameter(parameterContext, extensionContext); + }); + } - return context.get(paramType) - .orElseThrow(() -> new ParameterResolutionException("Failed to resolve parameter of type " - + paramType.getName())); + private void registrySetup(WebServerConfig.Builder builder) { + // there is a core service that is noop, there will be an injection service that will be op + GlobalServiceRegistry.registry() + .all(WebServerRegistryService.class) + .forEach(it -> it.updateBuilder(builder)); } private URI uri(Executable declaringExecutable, String socketName) { @@ -262,6 +296,7 @@ private void addRouting(WebServerConfig.Builder builder) { }); } + @SuppressWarnings({"rawtypes", "unchecked"}) private SetUpRouteHandler createRoutingMethodCall(Method method) { // @SetUpRoute may have parameters handled by different extensions List handlers = new ArrayList<>(); diff --git a/webserver/testing/junit5/junit5/src/main/java/io/helidon/webserver/testing/junit5/JunitExtensionBase.java b/webserver/testing/junit5/junit5/src/main/java/io/helidon/webserver/testing/junit5/JunitExtensionBase.java index 1c6c3d00e31..6421533f0c0 100644 --- a/webserver/testing/junit5/junit5/src/main/java/io/helidon/webserver/testing/junit5/JunitExtensionBase.java +++ b/webserver/testing/junit5/junit5/src/main/java/io/helidon/webserver/testing/junit5/JunitExtensionBase.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 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. @@ -21,18 +21,24 @@ import java.util.ArrayList; import java.util.List; +import io.helidon.testing.junit5.TestJunitExtension; + import org.junit.jupiter.api.extension.AfterAllCallback; import org.junit.jupiter.api.extension.ExtensionContext; -abstract class JunitExtensionBase implements AfterAllCallback { +abstract class JunitExtensionBase extends TestJunitExtension implements AfterAllCallback { private Class testClass; + JunitExtensionBase() { + } + @Override public void afterAll(ExtensionContext extensionContext) { callAfterStop(); + super.afterAll(extensionContext); } - void testClass(Class testClass) { + void testClass(Class testClass) { this.testClass = testClass; } diff --git a/webserver/testing/junit5/junit5/src/main/java/io/helidon/webserver/testing/junit5/spi/ServerJunitExtension.java b/webserver/testing/junit5/junit5/src/main/java/io/helidon/webserver/testing/junit5/spi/ServerJunitExtension.java index c9419f1b1b1..2c0c268aa55 100644 --- a/webserver/testing/junit5/junit5/src/main/java/io/helidon/webserver/testing/junit5/spi/ServerJunitExtension.java +++ b/webserver/testing/junit5/junit5/src/main/java/io/helidon/webserver/testing/junit5/spi/ServerJunitExtension.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 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. @@ -84,7 +84,7 @@ default Optional> setUpRouteParamHandler(Class type) { /** * Handler of server test parameters of methods annotated with {@link io.helidon.webserver.testing.junit5.SetUpRoute}. * - * @param + * @param type of the parameter this handler handles */ interface ParamHandler { /** diff --git a/webserver/testing/junit5/junit5/src/main/java/module-info.java b/webserver/testing/junit5/junit5/src/main/java/module-info.java index 3f3ad38967b..e927c2e369c 100644 --- a/webserver/testing/junit5/junit5/src/main/java/module-info.java +++ b/webserver/testing/junit5/junit5/src/main/java/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2022, 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. @@ -24,11 +24,15 @@ module io.helidon.webserver.testing.junit5 { requires io.helidon.logging.common; + requires io.helidon.service.registry; - requires transitive hamcrest.all; + requires transitive io.helidon.testing; + requires transitive io.helidon.testing.junit5; requires transitive io.helidon.common.testing.http.junit5; requires transitive io.helidon.webclient; requires transitive io.helidon.webserver; + + requires transitive hamcrest.all; requires transitive org.junit.jupiter.api; exports io.helidon.webserver.testing.junit5; diff --git a/webserver/webserver/pom.xml b/webserver/webserver/pom.xml index cb38365577e..44fd281d080 100644 --- a/webserver/webserver/pom.xml +++ b/webserver/webserver/pom.xml @@ -80,6 +80,10 @@ io.helidon.common.features helidon-common-features + + io.helidon.service + helidon-service-registry +