Skip to content

Commit

Permalink
refactor(test): provide better shaped junit extensions for testing ru…
Browse files Browse the repository at this point in the history
…ntimes (#4234)
  • Loading branch information
ndr-brt committed Jun 6, 2024
1 parent 703502f commit c582647
Show file tree
Hide file tree
Showing 26 changed files with 672 additions and 506 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ public class BaseRuntime {
private final ConfigurationLoader configurationLoader;
private final List<ServiceExtension> serviceExtensions = new ArrayList<>();
protected Monitor monitor;
protected ServiceExtensionContext context;

public BaseRuntime() {
this(new ServiceLocatorImpl());
Expand All @@ -73,22 +74,39 @@ protected BaseRuntime(ServiceLocator serviceLocator) {
public static void main(String[] args) {
programArgs = args;
var runtime = new BaseRuntime();
runtime.boot();
runtime.boot(true);
}

/**
* Main entry point to runtime initialization. Calls all methods
* and sets up a context shutdown hook at runtime shutdown.
* Main entry point to runtime initialization.
*
* @param addShutdownHook add a shutdown hook if true, don't otherwise.
*/
public void boot() {
boot(true);
}
public void boot(boolean addShutdownHook) {
monitor = createMonitor();
var config = configurationLoader.loadConfiguration(monitor);
context = createServiceExtensionContext(config);

/**
* Main entry point to runtime initialization. Calls all methods.
*/
public void bootWithoutShutdownHook() {
boot(false);
try {
var newExtensions = createExtensions(context);
bootExtensions(context, newExtensions);

newExtensions.stream().map(InjectionContainer::getInjectionTarget).forEach(serviceExtensions::add);
if (addShutdownHook) {
getRuntime().addShutdownHook(new Thread(this::shutdown));
}

if (context.hasService(HealthCheckService.class)) {
var startupStatusRef = new AtomicReference<>(HealthCheckResult.Builder.newInstance().component("BaseRuntime").success().build());
var healthCheckService = context.getService(HealthCheckService.class);
healthCheckService.addStartupStatusProvider(startupStatusRef::get);
}

} catch (Exception e) {
onError(e);
}

monitor.info(format("Runtime %s ready", context.getRuntimeId()));
}

/**
Expand Down Expand Up @@ -169,31 +187,4 @@ protected Monitor createMonitor() {
return ExtensionLoader.loadMonitor(programArgs);
}

private void boot(boolean addShutdownHook) {
monitor = createMonitor();
var config = configurationLoader.loadConfiguration(monitor);
var context = createServiceExtensionContext(config);

try {
var newExtensions = createExtensions(context);
bootExtensions(context, newExtensions);

newExtensions.stream().map(InjectionContainer::getInjectionTarget).forEach(serviceExtensions::add);
if (addShutdownHook) {
getRuntime().addShutdownHook(new Thread(this::shutdown));
}

if (context.hasService(HealthCheckService.class)) {
var startupStatusRef = new AtomicReference<>(HealthCheckResult.Builder.newInstance().component("BaseRuntime").success().build());
var healthCheckService = context.getService(HealthCheckService.class);
healthCheckService.addStartupStatusProvider(startupStatusRef::get);
}

} catch (Exception e) {
onError(e);
}

monitor.info(format("Runtime %s ready", context.getRuntimeId()));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public void initialize(ServiceExtensionContext context) {
void baseRuntime_shouldBoot() {
when(serviceLocator.loadImplementors(eq(ServiceExtension.class), anyBoolean())).thenReturn(List.of(new BaseExtension()));

runtime.boot();
runtime.boot(true);

verify(monitor, never()).severe(anyString(), any());
}
Expand All @@ -73,7 +73,7 @@ void baseRuntime_shouldNotBootWithException() {
doThrow(new EdcException("Failed to start base extension")).when(extension).start();
when(serviceLocator.loadImplementors(eq(ServiceExtension.class), anyBoolean())).thenReturn(List.of(extension));

assertThatThrownBy(runtime::boot).isInstanceOf(EdcException.class);
assertThatThrownBy(() -> runtime.boot(true)).isInstanceOf(EdcException.class);
verify(monitor).severe(startsWith("Error booting runtime: Failed to start base extension"), any(EdcException.class));
}

Expand All @@ -83,7 +83,7 @@ void shouldSetStartupCheckProvider_whenHealthCheckServiceIsRegistered() {
when(serviceLocator.loadImplementors(eq(ServiceExtension.class), anyBoolean())).thenReturn(List.of(
new BaseExtension(), registerService(HealthCheckService.class, healthCheckService)));

runtime.boot();
runtime.boot(true);

verify(healthCheckService).addStartupStatusProvider(any());
}
Expand All @@ -92,7 +92,7 @@ void shouldSetStartupCheckProvider_whenHealthCheckServiceIsRegistered() {
void shouldLoadConfiguration() {
when(serviceLocator.loadImplementors(eq(ServiceExtension.class), anyBoolean())).thenReturn(List.of(new BaseExtension()));

runtime.boot();
runtime.boot(true);

verify(serviceLocator).loadImplementors(ConfigurationExtension.class, false);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@

/**
* Utility class that permits to run multiple EDC runtimes statically
*
* @deprecated please use either {@link RuntimePerMethodExtension} or {@link RuntimePerClassExtension}.
*/
@Deprecated(since = "0.7.0")
public class EdcClassRuntimesExtension implements BeforeAllCallback, AfterAllCallback {

public final EdcRuntimeExtension[] extensions;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,30 +15,19 @@

package org.eclipse.edc.junit.extensions;

import org.eclipse.edc.boot.system.DefaultServiceExtensionContext;
import org.eclipse.edc.boot.system.ServiceLocator;
import org.eclipse.edc.boot.system.ServiceLocatorImpl;
import org.eclipse.edc.boot.system.runtime.BaseRuntime;
import org.eclipse.edc.spi.EdcException;
import org.eclipse.edc.spi.monitor.Monitor;
import org.eclipse.edc.spi.system.ConfigurationExtension;
import org.eclipse.edc.spi.system.ServiceExtensionContext;
import org.eclipse.edc.spi.system.SystemExtension;
import org.eclipse.edc.spi.system.configuration.Config;
import org.eclipse.edc.spi.system.configuration.ConfigFactory;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.extension.AfterTestExecutionCallback;
import org.junit.jupiter.api.extension.BeforeTestExecutionCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolutionException;
import org.junit.jupiter.api.extension.ParameterResolver;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import static java.util.Collections.emptyMap;
import static org.eclipse.edc.util.types.Cast.cast;

/**
Expand All @@ -47,19 +36,19 @@
* injection of runtime services is supported.
* <p>
* If only basic dependency injection is needed, use {@link DependencyInjectionExtension} instead.
*
* @deprecated please use either {@link RuntimePerMethodExtension} or {@link RuntimePerClassExtension}.
*/
public class EdcExtension extends BaseRuntime implements BeforeTestExecutionCallback, AfterTestExecutionCallback, ParameterResolver {
private final LinkedHashMap<Class<?>, Object> serviceMocks = new LinkedHashMap<>();
private final MultiSourceServiceLocator serviceLocator;
private DefaultServiceExtensionContext context;
@Deprecated(since = "0.7.0")
public class EdcExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback, ParameterResolver {
protected final EmbeddedRuntime runtime;

public EdcExtension() {
this(new MultiSourceServiceLocator());
this(new EmbeddedRuntime("runtime", emptyMap()));
}

private EdcExtension(MultiSourceServiceLocator serviceLocator) {
super(serviceLocator);
this.serviceLocator = serviceLocator;
protected EdcExtension(EmbeddedRuntime runtime) {
this.runtime = runtime;
}

/**
Expand All @@ -69,39 +58,35 @@ private EdcExtension(MultiSourceServiceLocator serviceLocator) {
* @param mock the service mock
*/
public <T> void registerServiceMock(Class<T> type, T mock) {
serviceMocks.put(type, mock);
runtime.registerServiceMock(type, mock);
}

/**
* Registers a service extension with the runtime.
*/
public <T extends SystemExtension> void registerSystemExtension(Class<T> type, SystemExtension extension) {
serviceLocator.registerSystemExtension(type, extension);
runtime.registerSystemExtension(type, extension);
}

@Override
public void beforeTestExecution(ExtensionContext extensionContext) throws Exception {
bootWithoutShutdownHook();
public void beforeTestExecution(ExtensionContext extensionContext) {
runtime.boot(false);
}

@Override
public void afterTestExecution(ExtensionContext context) throws Exception {
shutdown();
// clear the systemExtensions map to prevent it from piling up between subsequent runs
serviceLocator.clearSystemExtensions();
}

public DefaultServiceExtensionContext getContext() {
return context;
public void afterTestExecution(ExtensionContext context) {
runtime.shutdown();
}

@Override
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException {
var type = parameterContext.getParameter().getParameterizedType();
if (type.equals(EdcExtension.class)) {
return true;
} else if (type.equals(EmbeddedRuntime.class)) {
return true;
} else if (type instanceof Class) {
return context.hasService(cast(type));
return runtime.getContext().hasService(cast(type));
}
return false;
}
Expand All @@ -111,8 +96,10 @@ public Object resolveParameter(ParameterContext parameterContext, ExtensionConte
var type = parameterContext.getParameter().getParameterizedType();
if (type.equals(EdcExtension.class)) {
return this;
} else if (type.equals(EmbeddedRuntime.class)) {
return runtime;
} else if (type instanceof Class) {
return context.getService(cast(type));
return runtime.getContext().getService(cast(type));
}
return null;
}
Expand All @@ -122,55 +109,7 @@ public void setConfiguration(Map<String, String> configuration) {
}

public <T> T getService(Class<T> clazz) {
return context.getService(clazz);
}

@Override
protected @NotNull ServiceExtensionContext createContext(Monitor monitor, Config config) {
context = new TestServiceExtensionContext(monitor, config, serviceMocks);
return context;
}

/**
* A service locator that allows additional extensions to be manually loaded by a test fixture. This locator return
* the union of registered extensions and extensions loaded by the delegate.
*/
private static class MultiSourceServiceLocator implements ServiceLocator {
private final ServiceLocator delegate = new ServiceLocatorImpl();
private final LinkedHashMap<Class<? extends SystemExtension>, List<SystemExtension>> systemExtensions;

MultiSourceServiceLocator() {
systemExtensions = new LinkedHashMap<>();
}

@Override
public <T> List<T> loadImplementors(Class<T> type, boolean required) {
List<T> extensions = cast(systemExtensions.getOrDefault(type, new ArrayList<>()));
extensions.addAll(delegate.loadImplementors(type, required));
return extensions;
}

/**
* This implementation will override singleton implementions found by the delegate.
*/
@Override
public <T> T loadSingletonImplementor(Class<T> type, boolean required) {
var extensions = systemExtensions.get(type);
if (extensions == null || extensions.isEmpty()) {
return delegate.loadSingletonImplementor(type, required);
} else if (extensions.size() > 1) {
throw new EdcException("Multiple extensions were registered for type: " + type.getName());
}
return type.cast(extensions.get(0));
}

public <T extends SystemExtension> void registerSystemExtension(Class<T> type, SystemExtension extension) {
systemExtensions.computeIfAbsent(type, k -> new ArrayList<>()).add(extension);
}

public void clearSystemExtensions() {
systemExtensions.clear();
}
return runtime.getService(clazz);
}

}
Loading

0 comments on commit c582647

Please sign in to comment.