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; package de.extio.game_engine.module;
import java.util.List;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -30,6 +32,9 @@ public abstract class AbstractModule {
} }
public List<ModuleExecutorCallbacks> executorCallbackSubscriptions() {
return null;
}
public void run() { 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 @Override
public void execute() { public void execute() {
this.runTasks(ModuleExecutorCallbacks.UI_PRE, module -> () -> {
// TODO Subscriptions for modules to run only on certain conditions
this.runTasks(module -> () -> {
if (module instanceof final AbstractClientModule clientModule && this.moduleManager.getModulesDisplayedClientModules().contains(clientModule)) { if (module instanceof final AbstractClientModule clientModule && this.moduleManager.getModulesDisplayedClientModules().contains(clientModule)) {
this.invokeSafe(clientModule, AbstractClientModule::runUiPre); this.invokeSafe(clientModule, AbstractClientModule::runUiPre);
} }
}); });
this.runTasks(module -> () -> { this.runTasks(ModuleExecutorCallbacks.RUN, module -> () -> {
this.invokeSafe(module, AbstractModule::run); 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)) { if (module instanceof final AbstractClientModule clientModule && this.moduleManager.getModulesDisplayedClientModules().contains(clientModule)) {
this.invokeSafe(clientModule, AbstractClientModule::runUiPost); 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())) { 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); final var task = taskSupplier.apply(module);
if (task != null) { if (task != null) {
scope.fork(task); scope.fork(task);
} }
} }
}
scope.join(); scope.join();
} }
catch (final InterruptedException e) { catch (final InterruptedException e) {

View File

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

View File

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

View File

@@ -24,9 +24,11 @@ public class ModuleExecutorImplTest {
} }
@Test @Test
public void testExecute_CallsAllPhasesForActiveAndDisplayedClientModule() { public void testExecute_CallsAllPhasesForDisplayedClientModuleSubscribedToAllPhases() {
final AbstractClientModule clientModule = mock(AbstractClientModule.class); 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)); when(this.moduleManager.getModulesDisplayedClientModules()).thenReturn(List.of(clientModule));
this.moduleExecutor.execute(); this.moduleExecutor.execute();
@@ -37,20 +39,24 @@ public class ModuleExecutorImplTest {
} }
@Test @Test
public void testExecute_CallsOnlyRunForActiveNonClientModule() { public void testExecute_CallsOnlyRunForNonClientModule() {
final AbstractModule module = mock(AbstractModule.class); 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(); this.moduleExecutor.execute();
verify(module).run(); verify(module).run();
verify(module, never()).onActivate(); // Just checking it doesn't call other things verify(module, never()).onActivate();
} }
@Test @Test
public void testExecute_CallsOnlyRunForActiveButNotDisplayedClientModule() { public void testExecute_SkipsUiPhaseForNotDisplayedClientModule() {
final AbstractClientModule clientModule = mock(AbstractClientModule.class); 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()); when(this.moduleManager.getModulesDisplayedClientModules()).thenReturn(List.of());
this.moduleExecutor.execute(); this.moduleExecutor.execute();
@@ -61,12 +67,14 @@ public class ModuleExecutorImplTest {
} }
@Test @Test
public void testExecute_HandlesExceptionInModule() { public void testExecute_HandlesExceptionInRunPhase() {
final AbstractModule module1 = mock(AbstractModule.class); final AbstractModule module1 = mock(AbstractModule.class);
final AbstractModule module2 = mock(AbstractModule.class); final AbstractModule module2 = mock(AbstractModule.class);
doThrow(new RuntimeException("Test Exception")).when(module1).run(); 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(); this.moduleExecutor.execute();
@@ -79,7 +87,9 @@ public class ModuleExecutorImplTest {
final AbstractClientModule clientModule = mock(AbstractClientModule.class); final AbstractClientModule clientModule = mock(AbstractClientModule.class);
doThrow(new RuntimeException("UI Pre Exception")).when(clientModule).runUiPre(); 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)); when(this.moduleManager.getModulesDisplayedClientModules()).thenReturn(List.of(clientModule));
this.moduleExecutor.execute(); this.moduleExecutor.execute();
@@ -90,36 +100,226 @@ public class ModuleExecutorImplTest {
} }
@Test @Test
public void testExecute_EmptyModules() { public void testExecute_NoSubscribersForCallback() {
when(this.moduleManager.getModulesActive()).thenReturn(List.of()); 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(); 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 @Test
public void testExecute_MultipleModules() { public void testExecute_MultipleModulesWithMixedSubscriptions() {
final AbstractClientModule clientModule1 = mock(AbstractClientModule.class); final AbstractClientModule clientModule1 = mock(AbstractClientModule.class);
final AbstractClientModule clientModule2 = mock(AbstractClientModule.class); final AbstractClientModule clientModule2 = mock(AbstractClientModule.class);
final AbstractModule module1 = mock(AbstractModule.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)); when(this.moduleManager.getModulesDisplayedClientModules()).thenReturn(List.of(clientModule1));
this.moduleExecutor.execute(); this.moduleExecutor.execute();
// clientModule1: displayed, so all phases
verify(clientModule1).runUiPre(); verify(clientModule1).runUiPre();
verify(clientModule1).run(); verify(clientModule1).run();
verify(clientModule1).runUiPost(); verify(clientModule1).runUiPost();
// clientModule2: active but not displayed, so only run
verify(clientModule2, never()).runUiPre(); verify(clientModule2, never()).runUiPre();
verify(clientModule2).run(); verify(clientModule2).run();
verify(clientModule2, never()).runUiPost(); verify(clientModule2, never()).runUiPost();
// module1: only run
verify(module1).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()); 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 { static class TestModule extends AbstractModule {
boolean onLoadCalled = false; 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
);
}
}
} }