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
+