feat(module): Implement callback subscription mechanism for module execution phases

This commit is contained in:
extio-de
2025-12-23 17:27:00 +01:00
parent ff9dcbe71b
commit a0b8fa49c1
7 changed files with 484 additions and 102 deletions

View File

@@ -1,5 +1,7 @@
package de.extio.game_engine.module;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -30,6 +32,9 @@ public abstract class AbstractModule {
}
public List<ModuleExecutorCallbacks> executorCallbackSubscriptions() {
return null;
}
public void run() {

View File

@@ -0,0 +1,11 @@
package de.extio.game_engine.module;
public enum ModuleExecutorCallbacks {
UI_PRE,
RUN,
UI_POST;
}

View File

@@ -20,18 +20,15 @@ public class ModuleExecutorImpl implements ModuleExecutor {
@Override
public void execute() {
// TODO Subscriptions for modules to run only on certain conditions
this.runTasks(module -> () -> {
this.runTasks(ModuleExecutorCallbacks.UI_PRE, module -> () -> {
if (module instanceof final AbstractClientModule clientModule && this.moduleManager.getModulesDisplayedClientModules().contains(clientModule)) {
this.invokeSafe(clientModule, AbstractClientModule::runUiPre);
}
});
this.runTasks(module -> () -> {
this.runTasks(ModuleExecutorCallbacks.RUN, module -> () -> {
this.invokeSafe(module, AbstractModule::run);
});
this.runTasks(module -> () -> {
this.runTasks(ModuleExecutorCallbacks.UI_POST, module -> () -> {
if (module instanceof final AbstractClientModule clientModule && this.moduleManager.getModulesDisplayedClientModules().contains(clientModule)) {
this.invokeSafe(clientModule, AbstractClientModule::runUiPost);
}
@@ -47,14 +44,21 @@ public class ModuleExecutorImpl implements ModuleExecutor {
}
}
private void runTasks(final Function<AbstractModule, Runnable> taskSupplier) {
private void runTasks(final ModuleExecutorCallbacks callbackType, final Function<AbstractModule, Runnable> taskSupplier) {
final var subscribers = this.moduleManager.getSubscribersForCallback(callbackType);
if (subscribers.isEmpty()) {
return;
}
try (var scope = StructuredTaskScope.open(Joiner.awaitAll())) {
for (final var module : this.moduleManager.getModulesActive()) {
synchronized (subscribers) {
for (final var module : subscribers) {
final var task = taskSupplier.apply(module);
if (task != null) {
scope.fork(task);
}
}
}
scope.join();
}
catch (final InterruptedException e) {

View File

@@ -38,4 +38,6 @@ public interface ModuleManager {
boolean isModal();
List<AbstractModule> getSubscribersForCallback(ModuleExecutorCallbacks callback);
}

View File

@@ -1,9 +1,13 @@
package de.extio.game_engine.module;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Stack;
import java.util.function.Consumer;
import org.slf4j.Logger;
@@ -18,7 +22,7 @@ public class ModuleManagerImpl implements InitializingBean, ModuleManager {
private final List<AbstractModule> modulesInitial;
private final List<AbstractModule> modulesAll = new ArrayList<>();
private final List<AbstractModule> modulesAll = Collections.synchronizedList(new ArrayList<>());
private final RendererWorkingSet rendererWorkingSet;
@@ -30,11 +34,13 @@ public class ModuleManagerImpl implements InitializingBean, ModuleManager {
private List<AbstractClientModule> modulesDisplayedClientModulesView = List.of();
private final List<AbstractModule> modulesActive = new ArrayList<>();
private final List<AbstractModule> modulesActive = Collections.synchronizedList(new ArrayList<>());
private final List<AbstractClientModule> modulesDisplayed = new ArrayList<>();
private final List<AbstractClientModule> modulesDisplayed = Collections.synchronizedList(new ArrayList<>());
private final Deque<List<AbstractClientModule>> lastVisibleStates = new ArrayDeque<>();
private final Stack<List<AbstractClientModule>> lastVisibleStates = new Stack<>();
private final Map<ModuleExecutorCallbacks, List<AbstractModule>> executorCallbackMap = Collections.synchronizedMap(new EnumMap<>(ModuleExecutorCallbacks.class));
public ModuleManagerImpl(final List<AbstractModule> initialModules, final RendererWorkingSet rendererWorkingSet) {
this.modulesInitial = initialModules;
@@ -46,10 +52,7 @@ public class ModuleManagerImpl implements InitializingBean, ModuleManager {
if (this.modulesInitial == null || this.modulesInitial.isEmpty()) {
return;
}
this.modulesInitial.forEach(module -> this.invokeSafe(module, AbstractModule::onLoad));
this.modulesAll.addAll(this.modulesInitial);
this.sortModules(this.modulesAll);
this.modulesAllView = List.copyOf(this.modulesAll);
this.modulesInitial.forEach(this::load);
this.modulesInitial.clear();
}
@@ -74,30 +77,41 @@ public class ModuleManagerImpl implements InitializingBean, ModuleManager {
}
@Override
public void loadModule(final Class<? extends AbstractModule> clazz) {
public synchronized void loadModule(final Class<? extends AbstractModule> clazz) {
synchronized (this.modulesAll) {
if (this.modulesAll.stream().anyMatch(module -> clazz.isInstance(module))) {
LOGGER.debug("AbstractModule already loaded: " + clazz.getName());
return;
}
}
LOGGER.debug("Loading module " + clazz.getName());
try {
final AbstractModule module = AbstractModule.class.cast(clazz.getDeclaredConstructor().newInstance());
this.invokeSafe(module, m -> m.onLoad());
this.modulesAll.add(module);
this.sortModules(this.modulesAll);
this.modulesAllView = List.copyOf(this.modulesAll);
this.load(module);
}
catch (final Exception e) {
LOGGER.error("Error loading module " + clazz.getName(), e);
}
}
private void load(final AbstractModule module) {
this.invokeSafe(module, m -> m.onLoad());
this.modulesAll.add(module);
this.sortModules(this.modulesAll);
synchronized (this.modulesAll) {
this.modulesAllView = List.copyOf(this.modulesAll);
}
}
@Override
public void unloadModule(final Class<? extends AbstractModule> clazz) {
public synchronized void unloadModule(final Class<? extends AbstractModule> clazz) {
try {
final AbstractModule module = this.modulesAll.stream().filter(mod -> clazz.isInstance(mod)).findAny().orElse(null);
AbstractModule module;
synchronized (this.modulesAll) {
module = this.modulesAll.stream().filter(mod -> clazz.isInstance(mod)).findAny().orElse(null);
}
if (module == null) {
LOGGER.debug("AbstractModule not loaded: " + clazz.getName());
@@ -107,7 +121,9 @@ public class ModuleManagerImpl implements InitializingBean, ModuleManager {
this.changeActiveState(clazz, false);
this.modulesAll.remove(module);
this.sortModules(this.modulesAll);
synchronized (this.modulesAll) {
this.modulesAllView = List.copyOf(this.modulesAll);
}
this.invokeSafe(module, m -> m.onUnload());
if (module instanceof final AbstractClientModule clientModule) {
@@ -133,7 +149,7 @@ public class ModuleManagerImpl implements InitializingBean, ModuleManager {
}
@Override
public void changeActiveState(final Class<? extends AbstractModule> clazz, final boolean active) {
public synchronized void changeActiveState(final Class<? extends AbstractModule> clazz, final boolean active) {
this.modulesActiveView.stream()
.filter(module -> clazz.isAssignableFrom(module.getClass()))
.findAny()
@@ -145,20 +161,30 @@ public class ModuleManagerImpl implements InitializingBean, ModuleManager {
this.invokeSafe(module, AbstractModule::onDeactivate);
this.modulesActive.remove(module);
this.sortModules(this.modulesActive);
synchronized (this.modulesActive) {
this.modulesActiveView = List.copyOf(this.modulesActive);
this.modulesActiveClientModulesView = this.modulesActive.stream()
.filter(m -> m instanceof AbstractClientModule)
.map(m -> (AbstractClientModule) m)
.toList();
this.modulesDisplayedClientModulesView = List.copyOf(this.modulesDisplayed);
}
if (module instanceof final AbstractClientModule clientModule) {
this.rendererWorkingSet.clear(clientModule.getClass());
}
synchronized (this.executorCallbackMap) {
final var subscriptions = module.executorCallbackSubscriptions();
if (subscriptions != null) {
for (final var subscription : subscriptions) {
executorCallbackMap.computeIfAbsent(subscription, k -> Collections.synchronizedList(new ArrayList<>())).remove(module);
}
}
}
LOGGER.debug("Deactivated " + clazz.getName());
}
}, () -> {
if (active) {
synchronized (this.modulesAll) {
this.modulesAll.stream()
.filter(module -> clazz.isAssignableFrom(module.getClass()))
.findFirst()
@@ -169,16 +195,26 @@ public class ModuleManagerImpl implements InitializingBean, ModuleManager {
}
this.modulesActive.add(module);
this.sortModules(this.modulesActive);
synchronized (this.modulesActive) {
this.modulesActiveView = List.copyOf(this.modulesActive);
this.modulesActiveClientModulesView = this.modulesActive.stream()
.filter(m -> m instanceof AbstractClientModule)
.map(m -> (AbstractClientModule) m)
.toList();
this.modulesDisplayedClientModulesView = List.copyOf(this.modulesDisplayed);
}
synchronized (this.executorCallbackMap) {
final var subscriptions = module.executorCallbackSubscriptions();
if (subscriptions != null) {
for (final var subscription : subscriptions) {
executorCallbackMap.computeIfAbsent(subscription, k -> Collections.synchronizedList(new ArrayList<>())).add(module);
}
}
}
LOGGER.debug("Activated " + clazz.getName());
});
}
}
});
}
@@ -202,7 +238,9 @@ public class ModuleManagerImpl implements InitializingBean, ModuleManager {
}
@Override
public void changeDisplayState(final Class<? extends AbstractClientModule> clazz, final boolean display) {
public synchronized void changeDisplayState(final Class<? extends AbstractClientModule> clazz, final boolean display) {
synchronized (this.modulesAll) {
synchronized (this.modulesDisplayed) {
for (final var module : this.modulesAll) {
if (module instanceof final AbstractClientModule clientModule && clazz.isAssignableFrom(module.getClass())) {
final boolean curDisplayed = this.modulesDisplayed.stream().anyMatch(mod -> clazz.isAssignableFrom(mod.getClass()));
@@ -224,6 +262,8 @@ public class ModuleManagerImpl implements InitializingBean, ModuleManager {
}
}
}
}
}
@Override
public boolean isDisplayed(final String className) {
@@ -239,9 +279,9 @@ public class ModuleManagerImpl implements InitializingBean, ModuleManager {
@Override
public boolean isDisplayed(final Class<? extends AbstractClientModule> clazz) {
final var modules = this.modulesDisplayed;
final var modules = this.modulesDisplayedClientModulesView;
for (int i = 0; i < modules.size(); i++) {
if (clazz.isAssignableFrom(this.modulesDisplayed.get(i).getClass())) {
if (clazz.isAssignableFrom(modules.get(i).getClass())) {
return true;
}
}
@@ -252,7 +292,8 @@ public class ModuleManagerImpl implements InitializingBean, ModuleManager {
public void hideAll() {
this.lastVisibleStates.clear();
for (final var module : List.copyOf(this.modulesDisplayed)) {
final var modules = this.modulesDisplayedClientModulesView;
for (final var module : modules) {
if ((module instanceof final AbstractClientModule clientModule) && !clientModule.isAlwaysDisplay()) {
this.changeDisplayState(clientModule.getClass(), false);
}
@@ -263,9 +304,10 @@ public class ModuleManagerImpl implements InitializingBean, ModuleManager {
@SafeVarargs
public final void hideExcept(final String... classNames) {
final List<AbstractClientModule> state = new ArrayList<>();
final var keep = new java.util.HashSet<>(java.util.Arrays.asList(classNames));
final var keep = new HashSet<>(Arrays.asList(classNames));
for (final var module : List.copyOf(this.modulesDisplayed)) {
final var modules = this.modulesDisplayedClientModulesView;
for (final var module : modules) {
if (module instanceof final AbstractClientModule clientModule &&
!clientModule.isAlwaysDisplay() &&
!keep.contains(clientModule.getClass().getName())) {
@@ -285,16 +327,23 @@ public class ModuleManagerImpl implements InitializingBean, ModuleManager {
return;
}
synchronized (this.lastVisibleStates) {
for (final AbstractClientModule module : this.lastVisibleStates.pop()) {
this.changeDisplayState(module.getClass(), true);
}
}
}
@Override
public boolean isModal() {
return !this.lastVisibleStates.isEmpty();
}
@Override
public List<AbstractModule> getSubscribersForCallback(final ModuleExecutorCallbacks callback) {
return this.executorCallbackMap.getOrDefault(callback, List.of());
}
private void invokeSafe(final AbstractModule module, final Consumer<AbstractModule> consumer) {
try {
consumer.accept(module);

View File

@@ -24,9 +24,11 @@ public class ModuleExecutorImplTest {
}
@Test
public void testExecute_CallsAllPhasesForActiveAndDisplayedClientModule() {
public void testExecute_CallsAllPhasesForDisplayedClientModuleSubscribedToAllPhases() {
final AbstractClientModule clientModule = mock(AbstractClientModule.class);
when(this.moduleManager.getModulesActive()).thenReturn(List.of(clientModule));
when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_PRE)).thenReturn(List.of(clientModule));
when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.RUN)).thenReturn(List.of(clientModule));
when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_POST)).thenReturn(List.of(clientModule));
when(this.moduleManager.getModulesDisplayedClientModules()).thenReturn(List.of(clientModule));
this.moduleExecutor.execute();
@@ -37,20 +39,24 @@ public class ModuleExecutorImplTest {
}
@Test
public void testExecute_CallsOnlyRunForActiveNonClientModule() {
public void testExecute_CallsOnlyRunForNonClientModule() {
final AbstractModule module = mock(AbstractModule.class);
when(this.moduleManager.getModulesActive()).thenReturn(List.of(module));
when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_PRE)).thenReturn(List.of());
when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.RUN)).thenReturn(List.of(module));
when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_POST)).thenReturn(List.of());
this.moduleExecutor.execute();
verify(module).run();
verify(module, never()).onActivate(); // Just checking it doesn't call other things
verify(module, never()).onActivate();
}
@Test
public void testExecute_CallsOnlyRunForActiveButNotDisplayedClientModule() {
public void testExecute_SkipsUiPhaseForNotDisplayedClientModule() {
final AbstractClientModule clientModule = mock(AbstractClientModule.class);
when(this.moduleManager.getModulesActive()).thenReturn(List.of(clientModule));
when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_PRE)).thenReturn(List.of(clientModule));
when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.RUN)).thenReturn(List.of(clientModule));
when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_POST)).thenReturn(List.of(clientModule));
when(this.moduleManager.getModulesDisplayedClientModules()).thenReturn(List.of());
this.moduleExecutor.execute();
@@ -61,12 +67,14 @@ public class ModuleExecutorImplTest {
}
@Test
public void testExecute_HandlesExceptionInModule() {
public void testExecute_HandlesExceptionInRunPhase() {
final AbstractModule module1 = mock(AbstractModule.class);
final AbstractModule module2 = mock(AbstractModule.class);
doThrow(new RuntimeException("Test Exception")).when(module1).run();
when(this.moduleManager.getModulesActive()).thenReturn(List.of(module1, module2));
when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_PRE)).thenReturn(List.of());
when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.RUN)).thenReturn(List.of(module1, module2));
when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_POST)).thenReturn(List.of());
this.moduleExecutor.execute();
@@ -79,7 +87,9 @@ public class ModuleExecutorImplTest {
final AbstractClientModule clientModule = mock(AbstractClientModule.class);
doThrow(new RuntimeException("UI Pre Exception")).when(clientModule).runUiPre();
when(this.moduleManager.getModulesActive()).thenReturn(List.of(clientModule));
when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_PRE)).thenReturn(List.of(clientModule));
when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.RUN)).thenReturn(List.of(clientModule));
when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_POST)).thenReturn(List.of(clientModule));
when(this.moduleManager.getModulesDisplayedClientModules()).thenReturn(List.of(clientModule));
this.moduleExecutor.execute();
@@ -90,36 +100,226 @@ public class ModuleExecutorImplTest {
}
@Test
public void testExecute_EmptyModules() {
when(this.moduleManager.getModulesActive()).thenReturn(List.of());
public void testExecute_NoSubscribersForCallback() {
when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_PRE)).thenReturn(List.of());
when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.RUN)).thenReturn(List.of());
when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_POST)).thenReturn(List.of());
this.moduleExecutor.execute();
verify(this.moduleManager, times(3)).getModulesActive();
verify(this.moduleManager).getSubscribersForCallback(ModuleExecutorCallbacks.UI_PRE);
verify(this.moduleManager).getSubscribersForCallback(ModuleExecutorCallbacks.RUN);
verify(this.moduleManager).getSubscribersForCallback(ModuleExecutorCallbacks.UI_POST);
}
@Test
public void testExecute_MultipleModules() {
public void testExecute_MultipleModulesWithMixedSubscriptions() {
final AbstractClientModule clientModule1 = mock(AbstractClientModule.class);
final AbstractClientModule clientModule2 = mock(AbstractClientModule.class);
final AbstractModule module1 = mock(AbstractModule.class);
when(this.moduleManager.getModulesActive()).thenReturn(List.of(clientModule1, clientModule2, module1));
when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_PRE)).thenReturn(List.of(clientModule1, clientModule2));
when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.RUN)).thenReturn(List.of(clientModule1, clientModule2, module1));
when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_POST)).thenReturn(List.of(clientModule1));
when(this.moduleManager.getModulesDisplayedClientModules()).thenReturn(List.of(clientModule1));
this.moduleExecutor.execute();
// clientModule1: displayed, so all phases
verify(clientModule1).runUiPre();
verify(clientModule1).run();
verify(clientModule1).runUiPost();
// clientModule2: active but not displayed, so only run
verify(clientModule2, never()).runUiPre();
verify(clientModule2).run();
verify(clientModule2, never()).runUiPost();
// module1: only run
verify(module1).run();
}
@Test
public void testExecute_ModuleSubscribedOnlyToUiPre() {
final AbstractClientModule clientModule = mock(AbstractClientModule.class);
when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_PRE)).thenReturn(List.of(clientModule));
when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.RUN)).thenReturn(List.of());
when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_POST)).thenReturn(List.of());
when(this.moduleManager.getModulesDisplayedClientModules()).thenReturn(List.of(clientModule));
this.moduleExecutor.execute();
verify(clientModule).runUiPre();
verify(clientModule, never()).run();
verify(clientModule, never()).runUiPost();
}
@Test
public void testExecute_ModuleSubscribedOnlyToRun() {
final AbstractModule module = mock(AbstractModule.class);
when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_PRE)).thenReturn(List.of());
when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.RUN)).thenReturn(List.of(module));
when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_POST)).thenReturn(List.of());
this.moduleExecutor.execute();
verify(module).run();
}
@Test
public void testExecute_ClientModuleSubscribedOnlyToUiPost() {
final AbstractClientModule clientModule = mock(AbstractClientModule.class);
when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_PRE)).thenReturn(List.of());
when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.RUN)).thenReturn(List.of());
when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_POST)).thenReturn(List.of(clientModule));
when(this.moduleManager.getModulesDisplayedClientModules()).thenReturn(List.of(clientModule));
this.moduleExecutor.execute();
verify(clientModule, never()).runUiPre();
verify(clientModule, never()).run();
verify(clientModule).runUiPost();
}
@Test
public void testExecute_ClientModuleSubscribedToUiPreAndRun_NotUiPost() {
final AbstractClientModule clientModule = mock(AbstractClientModule.class);
when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_PRE)).thenReturn(List.of(clientModule));
when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.RUN)).thenReturn(List.of(clientModule));
when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_POST)).thenReturn(List.of());
when(this.moduleManager.getModulesDisplayedClientModules()).thenReturn(List.of(clientModule));
this.moduleExecutor.execute();
verify(clientModule).runUiPre();
verify(clientModule).run();
verify(clientModule, never()).runUiPost();
}
@Test
public void testExecute_ClientModuleSubscribedToUiPreAndUiPost_NotRun() {
final AbstractClientModule clientModule = mock(AbstractClientModule.class);
when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_PRE)).thenReturn(List.of(clientModule));
when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.RUN)).thenReturn(List.of());
when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_POST)).thenReturn(List.of(clientModule));
when(this.moduleManager.getModulesDisplayedClientModules()).thenReturn(List.of(clientModule));
this.moduleExecutor.execute();
verify(clientModule).runUiPre();
verify(clientModule, never()).run();
verify(clientModule).runUiPost();
}
@Test
public void testExecute_ClientModuleSubscribedToRunAndUiPost_NotUiPre() {
final AbstractClientModule clientModule = mock(AbstractClientModule.class);
when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_PRE)).thenReturn(List.of());
when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.RUN)).thenReturn(List.of(clientModule));
when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_POST)).thenReturn(List.of(clientModule));
when(this.moduleManager.getModulesDisplayedClientModules()).thenReturn(List.of(clientModule));
this.moduleExecutor.execute();
verify(clientModule, never()).runUiPre();
verify(clientModule).run();
verify(clientModule).runUiPost();
}
@Test
public void testExecute_HandleExceptionInUiPost() {
final AbstractClientModule clientModule = mock(AbstractClientModule.class);
doThrow(new RuntimeException("UI Post Exception")).when(clientModule).runUiPost();
when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_PRE)).thenReturn(List.of(clientModule));
when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.RUN)).thenReturn(List.of(clientModule));
when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_POST)).thenReturn(List.of(clientModule));
when(this.moduleManager.getModulesDisplayedClientModules()).thenReturn(List.of(clientModule));
this.moduleExecutor.execute();
verify(clientModule).runUiPre();
verify(clientModule).run();
verify(clientModule).runUiPost();
}
@Test
public void testExecute_HandleExceptionInRun() {
final AbstractClientModule clientModule = mock(AbstractClientModule.class);
doThrow(new RuntimeException("Run Exception")).when(clientModule).run();
when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_PRE)).thenReturn(List.of(clientModule));
when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.RUN)).thenReturn(List.of(clientModule));
when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_POST)).thenReturn(List.of(clientModule));
when(this.moduleManager.getModulesDisplayedClientModules()).thenReturn(List.of(clientModule));
this.moduleExecutor.execute();
verify(clientModule).runUiPre();
verify(clientModule).run();
verify(clientModule).runUiPost();
}
@Test
public void testExecute_MultipleExceptionsInDifferentPhases() {
final AbstractModule module1 = mock(AbstractModule.class);
final AbstractModule module2 = mock(AbstractModule.class);
final AbstractClientModule clientModule = mock(AbstractClientModule.class);
doThrow(new RuntimeException("Exception in module1")).when(module1).run();
doThrow(new RuntimeException("Exception in clientModule")).when(clientModule).runUiPre();
when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_PRE)).thenReturn(List.of(clientModule));
when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.RUN)).thenReturn(List.of(module1, module2));
when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_POST)).thenReturn(List.of());
when(this.moduleManager.getModulesDisplayedClientModules()).thenReturn(List.of(clientModule));
this.moduleExecutor.execute();
verify(clientModule).runUiPre();
verify(module1).run();
verify(module2).run();
}
@Test
public void testExecute_ClientModuleDisplayedButNotSubscribedToUiPhases() {
final AbstractClientModule clientModule = mock(AbstractClientModule.class);
when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_PRE)).thenReturn(List.of());
when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.RUN)).thenReturn(List.of(clientModule));
when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_POST)).thenReturn(List.of());
this.moduleExecutor.execute();
verify(clientModule, never()).runUiPre();
verify(clientModule).run();
verify(clientModule, never()).runUiPost();
}
@Test
public void testExecute_ClientModuleNotDisplayedButSubscribedToRunOnly() {
final AbstractClientModule clientModule = mock(AbstractClientModule.class);
when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_PRE)).thenReturn(List.of());
when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.RUN)).thenReturn(List.of(clientModule));
when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_POST)).thenReturn(List.of());
this.moduleExecutor.execute();
verify(clientModule, never()).runUiPre();
verify(clientModule).run();
verify(clientModule, never()).runUiPost();
}
@Test
public void testExecute_MultipleNonClientModulesSubscribedToDifferentCallbacks() {
final AbstractModule module1 = mock(AbstractModule.class);
final AbstractModule module2 = mock(AbstractModule.class);
final AbstractModule module3 = mock(AbstractModule.class);
when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_PRE)).thenReturn(List.of(module1));
when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.RUN)).thenReturn(List.of(module1, module2, module3));
when(this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_POST)).thenReturn(List.of(module3));
this.moduleExecutor.execute();
verify(module1).run();
verify(module2).run();
verify(module3).run();
}
}

View File

@@ -408,6 +408,95 @@ public class ModuleManagerTest {
assertFalse(this.moduleManager.isModal());
}
@Test
public void testGetSubscribersForCallback_NoSubscribers() {
final List<AbstractModule> subscribers = this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_PRE);
assertTrue(subscribers.isEmpty());
}
@Test
public void testGetSubscribersForCallback_WithSubscriber() {
this.moduleManager.loadModule(CallbackSubscriberModule.class);
this.moduleManager.changeActiveState(CallbackSubscriberModule.class, true);
final List<AbstractModule> subscribers = this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_PRE);
assertEquals(1, subscribers.size());
assertTrue(subscribers.stream().anyMatch(m -> m instanceof CallbackSubscriberModule));
}
@Test
public void testGetSubscribersForCallback_MultipleCallbacks() {
this.moduleManager.loadModule(MultiCallbackSubscriberModule.class);
this.moduleManager.changeActiveState(MultiCallbackSubscriberModule.class, true);
final List<AbstractModule> uiPreSubscribers = this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_PRE);
final List<AbstractModule> runSubscribers = this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.RUN);
final List<AbstractModule> uiPostSubscribers = this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_POST);
assertEquals(1, uiPreSubscribers.size());
assertEquals(1, runSubscribers.size());
assertEquals(1, uiPostSubscribers.size());
assertTrue(uiPreSubscribers.get(0) instanceof MultiCallbackSubscriberModule);
assertTrue(runSubscribers.get(0) instanceof MultiCallbackSubscriberModule);
assertTrue(uiPostSubscribers.get(0) instanceof MultiCallbackSubscriberModule);
}
@Test
public void testGetSubscribersForCallback_UnsubscribedCallbackEmpty() {
this.moduleManager.loadModule(CallbackSubscriberModule.class);
this.moduleManager.changeActiveState(CallbackSubscriberModule.class, true);
final List<AbstractModule> runSubscribers = this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.RUN);
assertTrue(runSubscribers.isEmpty());
}
@Test
public void testGetSubscribersForCallback_RemovedOnDeactivate() {
this.moduleManager.loadModule(CallbackSubscriberModule.class);
this.moduleManager.changeActiveState(CallbackSubscriberModule.class, true);
List<AbstractModule> subscribers = this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_PRE);
assertEquals(1, subscribers.size());
this.moduleManager.changeActiveState(CallbackSubscriberModule.class, false);
subscribers = this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_PRE);
assertTrue(subscribers.isEmpty());
}
@Test
public void testGetSubscribersForCallback_MultipleModules() {
this.moduleManager.loadModule(CallbackSubscriberModule.class);
this.moduleManager.loadModule(MultiCallbackSubscriberModule.class);
this.moduleManager.changeActiveState(CallbackSubscriberModule.class, true);
this.moduleManager.changeActiveState(MultiCallbackSubscriberModule.class, true);
final List<AbstractModule> uiPreSubscribers = this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_PRE);
assertEquals(2, uiPreSubscribers.size());
assertTrue(uiPreSubscribers.stream().anyMatch(m -> m instanceof CallbackSubscriberModule));
assertTrue(uiPreSubscribers.stream().anyMatch(m -> m instanceof MultiCallbackSubscriberModule));
}
@Test
public void testGetSubscribersForCallback_ReactivateModule() {
this.moduleManager.loadModule(CallbackSubscriberModule.class);
this.moduleManager.changeActiveState(CallbackSubscriberModule.class, true);
this.moduleManager.changeActiveState(CallbackSubscriberModule.class, false);
List<AbstractModule> subscribers = this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_PRE);
assertTrue(subscribers.isEmpty());
this.moduleManager.changeActiveState(CallbackSubscriberModule.class, true);
subscribers = this.moduleManager.getSubscribersForCallback(ModuleExecutorCallbacks.UI_PRE);
assertEquals(1, subscribers.size());
}
static class TestModule extends AbstractModule {
boolean onLoadCalled = false;
@@ -504,4 +593,26 @@ public class ModuleManagerTest {
}
static class CallbackSubscriberModule extends AbstractModule {
@Override
public java.util.List<ModuleExecutorCallbacks> executorCallbackSubscriptions() {
return java.util.List.of(ModuleExecutorCallbacks.UI_PRE);
}
}
static class MultiCallbackSubscriberModule extends AbstractModule {
@Override
public java.util.List<ModuleExecutorCallbacks> executorCallbackSubscriptions() {
return java.util.List.of(
ModuleExecutorCallbacks.UI_PRE,
ModuleExecutorCallbacks.RUN,
ModuleExecutorCallbacks.UI_POST
);
}
}
}