mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 05:50:39 +00:00
Command Corrections.
This commit is contained in:
1 parent
1e1eaa96aa
commit
b18681a3e2
43 files changed
+1227
-251
No files matched your search
@@ -97,6 +97,59 @@ public class EngineShutdownDrainTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void settledStructureBootstrapFailureDoesNotPreventEngineCleanup() throws Exception {
|
||||
ShutdownFixture fixture = new ShutdownFixture();
|
||||
NativeStructureBootstrapBarrier barrier = new NativeStructureBootstrapBarrier();
|
||||
setField(fixture.engine, "nativeStructureBootstrapBarrier", barrier);
|
||||
CompletableFuture<Void> rings = new CompletableFuture<>();
|
||||
barrier.start(() -> {
|
||||
}, () -> rings);
|
||||
rings.completeExceptionally(new IllegalStateException("Ring worker failed"));
|
||||
doCallRealMethod().when(fixture.engine).drainNativeStructureBootstrap();
|
||||
|
||||
try (MockedStatic<NativeStructureOwnershipStore> ownership = mockStatic(NativeStructureOwnershipStore.class)) {
|
||||
fixture.shutdown.close();
|
||||
}
|
||||
|
||||
verify(fixture.engine).closeAttachedGenerationHistoryRuntimeRouter();
|
||||
verify(fixture.mantle).close();
|
||||
assertTrue(fixture.engine.closed);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void untrackedStructureBootstrapKeepsItsRuntimeAndRouterOpen() throws Exception {
|
||||
ShutdownFixture fixture = new ShutdownFixture();
|
||||
doThrow(new IllegalStateException("Bootstrap could not be tracked"))
|
||||
.when(fixture.engine).drainNativeStructureBootstrap();
|
||||
|
||||
assertThrows(IllegalStateException.class, fixture.shutdown::close);
|
||||
|
||||
verify(fixture.engine, never()).closeAttachedGenerationHistoryRuntimeRouter();
|
||||
verify(fixture.mantle, never()).close();
|
||||
assertFalse(fixture.engine.closed);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void structureBootstrapDrainsBeforeItsHistoryRouterCloses() throws Exception {
|
||||
ShutdownFixture fixture = new ShutdownFixture();
|
||||
AtomicBoolean ringsRunning = new AtomicBoolean(true);
|
||||
doAnswer(invocation -> {
|
||||
assertFalse(Thread.holdsLock(fixture.engine.lifecycleLock));
|
||||
ringsRunning.set(false);
|
||||
return null;
|
||||
}).when(fixture.engine).drainNativeStructureBootstrap();
|
||||
doAnswer(invocation -> {
|
||||
assertFalse("Ring workers still need their generation-history router", ringsRunning.get());
|
||||
return null;
|
||||
}).when(fixture.engine).closeAttachedGenerationHistoryRuntimeRouter();
|
||||
|
||||
try (MockedStatic<NativeStructureOwnershipStore> ownership = mockStatic(NativeStructureOwnershipStore.class)) {
|
||||
fixture.shutdown.close();
|
||||
}
|
||||
assertTrue(fixture.engine.closed);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void queuedWorldManagerRoutesAreCancelledBeforeRouterDrain() throws Exception {
|
||||
ShutdownFixture fixture = new ShutdownFixture();
|
||||
@@ -541,6 +594,11 @@ public class EngineShutdownDrainTest {
|
||||
ShutdownFixture fixture = new ShutdownFixture();
|
||||
IrisComplex replacement = mock(IrisComplex.class);
|
||||
fixture.prepareComplexHotload(replacement);
|
||||
doAnswer(invocation -> {
|
||||
assertFalse("Ring workers need the lifecycle lock while hotload waits",
|
||||
Thread.holdsLock(fixture.engine.lifecycleLock));
|
||||
return null;
|
||||
}).when(fixture.engine).awaitNativeStructureBootstrap("complex hotload");
|
||||
|
||||
new EngineHotloader(fixture.engine).hotloadComplex();
|
||||
|
||||
|
||||
+1
@@ -880,6 +880,7 @@ public class IrisEngineGenerationRuntimeScopeTest {
|
||||
setField(engine, "generationRuntimeRetirementListeners", new CopyOnWriteArraySet<IntConsumer>());
|
||||
setField(engine, "shutdownSequence", new EngineShutdownSequence(engine));
|
||||
setField(engine, "hotloader", new EngineHotloader(engine));
|
||||
setField(engine, "nativeStructureBootstrapBarrier", new NativeStructureBootstrapBarrier());
|
||||
setField(engine, "nativeStructureVolumeMemo", new NativeStructureVolumeMemo());
|
||||
setField(engine, "closing", new AtomicBoolean(false));
|
||||
engine.lifecycleState = IrisEngine.LifecycleState.RUNNING;
|
||||
|
||||
+62
@@ -10,10 +10,72 @@ import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class NativeStructureBootstrapBarrierTest {
|
||||
@Test
|
||||
public void failedRingWorkersCanBeReleasedOnceTheirCompletionSettles() {
|
||||
NativeStructureBootstrapBarrier barrier = new NativeStructureBootstrapBarrier();
|
||||
CompletableFuture<Void> rings = new CompletableFuture<>();
|
||||
IllegalStateException failure = new IllegalStateException("ring failure");
|
||||
barrier.start(() -> {
|
||||
}, () -> rings);
|
||||
rings.completeExceptionally(failure);
|
||||
|
||||
assertSame(failure, barrier.awaitForClose());
|
||||
assertFalse(barrier.isActive());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void untrackedStarterFailureDoesNotAllowResourceRelease() {
|
||||
NativeStructureBootstrapBarrier barrier = new NativeStructureBootstrapBarrier();
|
||||
assertThrows(IllegalStateException.class, () -> barrier.start(() -> {
|
||||
}, () -> {
|
||||
throw new IllegalStateException("partial scheduling");
|
||||
}));
|
||||
|
||||
assertThrows(IllegalStateException.class, barrier::awaitForClose);
|
||||
assertTrue(barrier.isActive());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exposedCompletionCannotReleaseRunningRingWork() {
|
||||
NativeStructureBootstrapBarrier barrier = new NativeStructureBootstrapBarrier();
|
||||
CompletableFuture<Void> rings = new CompletableFuture<>();
|
||||
CompletableFuture<Void> completion = barrier.start(() -> {
|
||||
}, () -> rings);
|
||||
|
||||
completion.complete(null);
|
||||
|
||||
assertTrue(barrier.isActive());
|
||||
rings.complete(null);
|
||||
assertFalse(barrier.isActive());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void waitingDoesNotHoldTheBarrierMonitor() throws Exception {
|
||||
NativeStructureBootstrapBarrier barrier = new NativeStructureBootstrapBarrier();
|
||||
CompletableFuture<Void> rings = new CompletableFuture<>();
|
||||
barrier.start(() -> {
|
||||
}, () -> rings);
|
||||
ExecutorService executor = Executors.newFixedThreadPool(2);
|
||||
try {
|
||||
Future<?> close = executor.submit(() -> barrier.await("close"));
|
||||
assertThrows(TimeoutException.class, () -> close.get(50L, TimeUnit.MILLISECONDS));
|
||||
Future<?> worker = executor.submit(() -> {
|
||||
assertTrue(barrier.isActive());
|
||||
rings.complete(null);
|
||||
});
|
||||
worker.get(1L, TimeUnit.SECONDS);
|
||||
close.get(1L, TimeUnit.SECONDS);
|
||||
} finally {
|
||||
rings.complete(null);
|
||||
executor.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void immediateCloseWaitsForExactRingCompletion() throws Exception {
|
||||
NativeStructureBootstrapBarrier barrier = new NativeStructureBootstrapBarrier();
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
package art.arcane.iris.studio.jigsaw;
|
||||
|
||||
import art.arcane.volmlib.util.event.ProtectionProbe;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.player.PlayerInteractEvent;
|
||||
import org.bukkit.inventory.EquipmentSlot;
|
||||
import org.bukkit.inventory.PlayerInventory;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class JigsawStudioProtectionProbeTest {
|
||||
@Test
|
||||
public void previewDoesNotMarkStudioDirty() throws Exception {
|
||||
JigsawStudioService service = new JigsawStudioService();
|
||||
JigsawStudioDirtyTracker tracker = mock(JigsawStudioDirtyTracker.class);
|
||||
JigsawStudioTileWatcher watcher = mock(JigsawStudioTileWatcher.class);
|
||||
setField(service, "dirtyTracker", tracker);
|
||||
setField(service, "tileWatcher", watcher);
|
||||
PlayerInteractEvent event = probe();
|
||||
|
||||
service.protectionListener.onPlayerInteract(event);
|
||||
|
||||
verify(tracker, never()).markDirty(event.getClickedBlock());
|
||||
verify(watcher, never()).finalizeJigsawTileWatchesForPlayer(event.getPlayer().getUniqueId());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void controlChestProbeRemainsDeniedWithoutOpeningItsMenu() throws Exception {
|
||||
JigsawStudioService service = new JigsawStudioService();
|
||||
JigsawStudioProtection protection = mock(JigsawStudioProtection.class);
|
||||
JigsawStudioToolbelt toolbelt = mock(JigsawStudioToolbelt.class);
|
||||
setField(service, "protection", protection);
|
||||
setField(service, "toolbelt", toolbelt);
|
||||
PlayerInteractEvent event = probe();
|
||||
when(protection.isControlChest(event.getClickedBlock())).thenReturn(true);
|
||||
try (MockedStatic<JigsawStudioProtection> authorization = mockStatic(JigsawStudioProtection.class)) {
|
||||
authorization.when(() -> JigsawStudioProtection.authorizeOwner(event.getPlayer(), null)).thenReturn(true);
|
||||
|
||||
service.protectionListener.onControlChest(event);
|
||||
|
||||
assertTrue(event.isCancelled());
|
||||
verify(toolbelt, never()).openControlMenu(any(Player.class));
|
||||
}
|
||||
}
|
||||
|
||||
private static PlayerInteractEvent probe() {
|
||||
Player player = mock(Player.class);
|
||||
World world = mock(World.class);
|
||||
when(world.getUID()).thenReturn(UUID.randomUUID());
|
||||
when(player.getWorld()).thenReturn(world);
|
||||
when(player.getUniqueId()).thenReturn(UUID.randomUUID());
|
||||
when(player.getInventory()).thenReturn(mock(PlayerInventory.class));
|
||||
return ProtectionProbe.blockInteract(player, mock(Block.class), EquipmentSlot.HAND);
|
||||
}
|
||||
|
||||
private static void setField(Object target, String name, Object value) throws Exception {
|
||||
Field field = target.getClass().getDeclaredField(name);
|
||||
field.setAccessible(true);
|
||||
field.set(target, value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package art.arcane.iris.studio.jigsaw;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.BlockFace;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.Event;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.block.Action;
|
||||
import org.bukkit.event.player.PlayerInteractEvent;
|
||||
import org.bukkit.inventory.EquipmentSlot;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import org.bukkit.plugin.RegisteredListener;
|
||||
import org.junit.Test;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class JigsawStudioToolInteractionTest {
|
||||
@Test
|
||||
public void airClickReachesTheToolDespiteBukkitsDefaultCancellation() throws Exception {
|
||||
Fixture fixture = fixture();
|
||||
PlayerInteractEvent event = event(fixture.player(), Action.RIGHT_CLICK_AIR, null);
|
||||
assertTrue(event.isCancelled());
|
||||
|
||||
dispatch(fixture.listener(), event);
|
||||
|
||||
verify(fixture.toolbelt()).useTool(fixture.player(), fixture.payload(), null, false);
|
||||
verify(fixture.watcher()).finalizeJigsawTileWatchesForPlayer(fixture.player().getUniqueId());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void deniedBlockAndItemInteractionsDoNotUseTheTool() throws Exception {
|
||||
Fixture fixture = fixture();
|
||||
PlayerInteractEvent block = event(fixture.player(), Action.RIGHT_CLICK_BLOCK, mock(Block.class));
|
||||
block.setUseInteractedBlock(Event.Result.DENY);
|
||||
dispatch(fixture.listener(), block);
|
||||
PlayerInteractEvent air = event(fixture.player(), Action.RIGHT_CLICK_AIR, null);
|
||||
air.setUseItemInHand(Event.Result.DENY);
|
||||
dispatch(fixture.listener(), air);
|
||||
PlayerInteractEvent item = event(fixture.player(), Action.RIGHT_CLICK_BLOCK, mock(Block.class));
|
||||
item.setUseItemInHand(Event.Result.DENY);
|
||||
dispatch(fixture.listener(), item);
|
||||
|
||||
verifyNoInteractions(fixture.codec(), fixture.toolbelt(), fixture.watcher());
|
||||
}
|
||||
|
||||
private static Fixture fixture() throws Exception {
|
||||
JigsawStudioService service = new JigsawStudioService();
|
||||
JigsawStudioToolbelt toolbelt = mock(JigsawStudioToolbelt.class);
|
||||
JigsawStudioToolCodec codec = mock(JigsawStudioToolCodec.class);
|
||||
JigsawStudioTileWatcher watcher = mock(JigsawStudioTileWatcher.class);
|
||||
JigsawStudioToolPayload payload = mock(JigsawStudioToolPayload.class);
|
||||
Player player = mock(Player.class);
|
||||
when(player.getUniqueId()).thenReturn(UUID.randomUUID());
|
||||
when(codec.decode((ItemStack) null)).thenReturn(Optional.of(payload));
|
||||
setField(service, "toolbelt", toolbelt);
|
||||
setField(service, "tileWatcher", watcher);
|
||||
setField(toolbelt, "toolCodec", codec);
|
||||
return new Fixture(service.protectionListener, toolbelt, codec, watcher, payload, player);
|
||||
}
|
||||
|
||||
private static PlayerInteractEvent event(Player player, Action action, Block block) {
|
||||
return new PlayerInteractEvent(player, action, null, block, BlockFace.UP, EquipmentSlot.HAND);
|
||||
}
|
||||
|
||||
private static void dispatch(JigsawStudioProtectionListener listener, PlayerInteractEvent event) throws Exception {
|
||||
EventHandler annotation = JigsawStudioProtectionListener.class
|
||||
.getMethod("onUseTool", PlayerInteractEvent.class).getAnnotation(EventHandler.class);
|
||||
RegisteredListener registered = new RegisteredListener(listener,
|
||||
(ignored, dispatched) -> listener.onUseTool((PlayerInteractEvent) dispatched),
|
||||
EventPriority.LOWEST, mock(Plugin.class), annotation.ignoreCancelled());
|
||||
registered.callEvent(event);
|
||||
}
|
||||
|
||||
private static void setField(Object target, String name, Object value) throws Exception {
|
||||
Field field = target.getClass().getDeclaredField(name);
|
||||
field.setAccessible(true);
|
||||
field.set(target, value);
|
||||
}
|
||||
|
||||
private record Fixture(JigsawStudioProtectionListener listener, JigsawStudioToolbelt toolbelt,
|
||||
JigsawStudioToolCodec codec, JigsawStudioTileWatcher watcher,
|
||||
JigsawStudioToolPayload payload, Player player) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package art.arcane.iris.studio.object;
|
||||
|
||||
import art.arcane.iris.platform.bukkit.nms.INMS;
|
||||
import art.arcane.iris.platform.bukkit.nms.INMSBinding;
|
||||
import art.arcane.iris.platform.generation.BukkitChunkGenerator;
|
||||
import art.arcane.iris.platform.generation.PlatformChunkGenerator;
|
||||
import art.arcane.iris.world.task.J;
|
||||
import org.bukkit.World;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class StudioBootstrapActivationTest {
|
||||
@Test(timeout = 5000L)
|
||||
public void activationReturnsPendingFutureAndFinishesOnlyAfterNativeRings() throws Exception {
|
||||
verifyActivation(false);
|
||||
}
|
||||
|
||||
@Test(timeout = 5000L)
|
||||
public void failedNativeRingsPreserveCauseAndDoNotEndBootstrap() throws Exception {
|
||||
verifyActivation(true);
|
||||
}
|
||||
|
||||
private void verifyActivation(boolean fail) throws Exception {
|
||||
BukkitChunkGenerator provider = mock(BukkitChunkGenerator.class);
|
||||
World world = mock(World.class);
|
||||
INMSBinding nms = mock(INMSBinding.class);
|
||||
CompletableFuture<Void> rings = new CompletableFuture<>();
|
||||
when(nms.completeStudioStructureBootstrap(world)).thenReturn(rings);
|
||||
try (MockedStatic<INMS> binding = mockStatic(INMS.class);
|
||||
MockedStatic<J> scheduling = mockStatic(J.class)) {
|
||||
binding.when(INMS::get).thenReturn(nms);
|
||||
scheduling.when(() -> J.sfut(any(Supplier.class))).thenAnswer(invocation -> {
|
||||
Supplier<?> supplier = invocation.getArgument(0);
|
||||
return CompletableFuture.completedFuture(supplier.get());
|
||||
});
|
||||
scheduling.when(() -> J.sfut(any(Runnable.class))).thenAnswer(invocation -> {
|
||||
Runnable runnable = invocation.getArgument(0);
|
||||
runnable.run();
|
||||
return CompletableFuture.completedFuture(null);
|
||||
});
|
||||
|
||||
CompletableFuture<Void> activation = activate(world, provider);
|
||||
assertFalse(activation.isDone());
|
||||
verify(provider, never()).endStudioEntryBootstrap();
|
||||
|
||||
if (fail) {
|
||||
IllegalStateException failure = new IllegalStateException("ring calculation failed");
|
||||
rings.completeExceptionally(failure);
|
||||
CompletionException reported = assertThrows(CompletionException.class, activation::join);
|
||||
assertSame(failure, reported.getCause());
|
||||
verify(provider, never()).endStudioEntryBootstrap();
|
||||
} else {
|
||||
rings.complete(null);
|
||||
activation.join();
|
||||
verify(provider).endStudioEntryBootstrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private CompletableFuture<Void> activate(World world, PlatformChunkGenerator provider) throws Exception {
|
||||
Method method = StudioOpenCoordinator.class.getDeclaredMethod(
|
||||
"endStudioEntryBootstrap", World.class, PlatformChunkGenerator.class);
|
||||
method.setAccessible(true);
|
||||
return (CompletableFuture<Void>) method.invoke(StudioOpenCoordinator.get(), world, provider);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package art.arcane.iris.studio.object;
|
||||
|
||||
import art.arcane.iris.pack.datapack.ServerConfigurator;
|
||||
import art.arcane.iris.platform.generation.PlatformChunkGenerator;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.studio.workspace.IrisProject;
|
||||
import art.arcane.iris.world.lifecycle.LifecycleOperationCoordinator;
|
||||
import org.bukkit.World;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class StudioCloseRecoveryTest {
|
||||
@Test
|
||||
public void failedGeneratorCloseKeepsMutationReservationWithoutRestartingServer() throws Exception {
|
||||
PlatformChunkGenerator provider = mock(PlatformChunkGenerator.class);
|
||||
IllegalStateException failure = new IllegalStateException("generator cleanup failed");
|
||||
when(provider.closeAsync()).thenReturn(CompletableFuture.failedFuture(failure));
|
||||
LifecycleOperationCoordinator coordinator = mock(LifecycleOperationCoordinator.class);
|
||||
LifecycleOperationCoordinator.Lease lease = mock(LifecycleOperationCoordinator.Lease.class);
|
||||
when(coordinator.acquire(any(), any(), anyString())).thenReturn(lease);
|
||||
|
||||
try (MockedStatic<LifecycleOperationCoordinator> lifecycle = mockStatic(LifecycleOperationCoordinator.class);
|
||||
MockedStatic<ServerConfigurator> configurator = mockStatic(ServerConfigurator.class);
|
||||
MockedStatic<IrisLogging> logging = mockStatic(IrisLogging.class)) {
|
||||
lifecycle.when(LifecycleOperationCoordinator::get).thenReturn(coordinator);
|
||||
|
||||
StudioOpenCoordinator.StudioCloseResult result = close(provider, null).join();
|
||||
|
||||
assertSame(failure, result.failureCause());
|
||||
configurator.verifyNoInteractions();
|
||||
verify(lease, never()).close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pendingGeneratorRetainsProjectAndMutationReservationUntilCleanupFinishes() throws Exception {
|
||||
PlatformChunkGenerator provider = mock(PlatformChunkGenerator.class);
|
||||
CompletableFuture<Void> generatorClose = new CompletableFuture<>();
|
||||
when(provider.closeAsync()).thenReturn(generatorClose);
|
||||
IrisProject project = mock(IrisProject.class);
|
||||
LifecycleOperationCoordinator coordinator = mock(LifecycleOperationCoordinator.class);
|
||||
LifecycleOperationCoordinator.Lease lease = mock(LifecycleOperationCoordinator.Lease.class);
|
||||
when(coordinator.acquire(any(), any(), anyString())).thenReturn(lease);
|
||||
|
||||
try (MockedStatic<LifecycleOperationCoordinator> lifecycle = mockStatic(LifecycleOperationCoordinator.class);
|
||||
MockedStatic<ServerConfigurator> configurator = mockStatic(ServerConfigurator.class);
|
||||
MockedStatic<IrisLogging> logging = mockStatic(IrisLogging.class)) {
|
||||
lifecycle.when(LifecycleOperationCoordinator::get).thenReturn(coordinator);
|
||||
|
||||
CompletableFuture<StudioOpenCoordinator.StudioCloseResult> close = close(provider, project);
|
||||
assertFalse(close.isDone());
|
||||
verifyNoInteractions(project);
|
||||
verify(lease, never()).close();
|
||||
|
||||
generatorClose.complete(null);
|
||||
close.join();
|
||||
|
||||
verify(project).setActiveProvider(null);
|
||||
verify(project).setActiveOpenKind(null);
|
||||
verify(lease).close();
|
||||
configurator.verifyNoInteractions();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cancellingCloseResultDoesNotSkipCleanupOrLeaseRelease() throws Exception {
|
||||
PlatformChunkGenerator provider = mock(PlatformChunkGenerator.class);
|
||||
CompletableFuture<Void> generatorClose = new CompletableFuture<>();
|
||||
when(provider.closeAsync()).thenReturn(generatorClose);
|
||||
IrisProject project = mock(IrisProject.class);
|
||||
LifecycleOperationCoordinator coordinator = mock(LifecycleOperationCoordinator.class);
|
||||
LifecycleOperationCoordinator.Lease lease = mock(LifecycleOperationCoordinator.Lease.class);
|
||||
when(coordinator.acquire(any(), any(), anyString())).thenReturn(lease);
|
||||
|
||||
try (MockedStatic<LifecycleOperationCoordinator> lifecycle = mockStatic(LifecycleOperationCoordinator.class);
|
||||
MockedStatic<ServerConfigurator> configurator = mockStatic(ServerConfigurator.class);
|
||||
MockedStatic<IrisLogging> logging = mockStatic(IrisLogging.class)) {
|
||||
lifecycle.when(LifecycleOperationCoordinator::get).thenReturn(coordinator);
|
||||
|
||||
CompletableFuture<StudioOpenCoordinator.StudioCloseResult> result = close(provider, project);
|
||||
assertTrue(result.cancel(false));
|
||||
assertFalse(generatorClose.isDone());
|
||||
verifyNoInteractions(project);
|
||||
verify(lease, never()).close();
|
||||
|
||||
generatorClose.complete(null);
|
||||
|
||||
assertTrue(result.isCancelled());
|
||||
verify(project).setActiveProvider(null);
|
||||
verify(project).setActiveOpenKind(null);
|
||||
verify(lease).close();
|
||||
configurator.verifyNoInteractions();
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private CompletableFuture<StudioOpenCoordinator.StudioCloseResult> close(
|
||||
PlatformChunkGenerator provider,
|
||||
IrisProject project
|
||||
) throws Exception {
|
||||
Method method = StudioOpenCoordinator.class.getDeclaredMethod("closeWorldCoordinated",
|
||||
PlatformChunkGenerator.class, String.class, World.class, boolean.class, IrisProject.class);
|
||||
method.setAccessible(true);
|
||||
return (CompletableFuture<StudioOpenCoordinator.StudioCloseResult>) method.invoke(
|
||||
StudioOpenCoordinator.get(), provider, null, null, false, project);
|
||||
}
|
||||
}
|
||||
+23
-36
@@ -8,9 +8,11 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
@@ -77,47 +79,32 @@ public class StudioOpenCoordinatorCloseSequenceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void terminalTimeoutPreventsLateUnloadFromClosingOrDeleting() {
|
||||
public void slowCloseKeepsWaitingAndFinishesRemainingPhasesWithoutRestart() {
|
||||
ArrayList<String> phases = new ArrayList<>();
|
||||
AtomicBoolean terminalTimeout = new AtomicBoolean(false);
|
||||
CompletableFuture<Void> unload = new CompletableFuture<>();
|
||||
AtomicReference<Runnable> warning = new AtomicReference<>();
|
||||
try (MockedStatic<ServerConfigurator> configurator = mockStatic(ServerConfigurator.class);
|
||||
MockedStatic<IrisLogging> logging = mockStatic(IrisLogging.class)) {
|
||||
CompletableFuture<Void> close = StudioOpenCoordinator.sequenceStudioClose(
|
||||
() -> phase(phases, "evacuate"),
|
||||
() -> {
|
||||
phases.add("unload");
|
||||
return unload;
|
||||
},
|
||||
() -> phase(phases, "close-generator"),
|
||||
() -> phase(phases, "delete-folders"));
|
||||
StudioOpenCoordinator.warnOnSlowClose(close, "studio-world", warning::set);
|
||||
|
||||
CompletableFuture<Void> close = StudioOpenCoordinator.sequenceStudioClose(
|
||||
() -> phase(phases, "evacuate"),
|
||||
() -> {
|
||||
phases.add("unload");
|
||||
return unload;
|
||||
},
|
||||
() -> phase(phases, "close-generator"),
|
||||
() -> phase(phases, "delete-folders"),
|
||||
terminalTimeout::get);
|
||||
warning.get().run();
|
||||
assertFalse(close.isDone());
|
||||
assertFalse(unload.isDone());
|
||||
assertEquals(List.of("evacuate", "unload"), phases);
|
||||
configurator.verifyNoInteractions();
|
||||
|
||||
terminalTimeout.set(true);
|
||||
unload.complete(null);
|
||||
try {
|
||||
unload.complete(null);
|
||||
close.join();
|
||||
fail("Expected terminal timeout");
|
||||
} catch (CompletionException exception) {
|
||||
assertEquals("Studio close stopped after its terminal timeout.", exception.getCause().getMessage());
|
||||
}
|
||||
assertEquals(List.of("evacuate", "unload"), phases);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void closeFailureAfterUnloadBeginsRequestsTerminalRestart() {
|
||||
try (MockedStatic<ServerConfigurator> configurator = mockStatic(ServerConfigurator.class)) {
|
||||
StudioOpenCoordinator.requestRestartAfterPartialClose("studio-world", true);
|
||||
|
||||
configurator.verify(() -> ServerConfigurator.restart(
|
||||
"Studio close failed after world unload began for \"studio-world\"."));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void closeFailureBeforeUnloadDoesNotRequestRestart() {
|
||||
try (MockedStatic<ServerConfigurator> configurator = mockStatic(ServerConfigurator.class)) {
|
||||
StudioOpenCoordinator.requestRestartAfterPartialClose("studio-world", false);
|
||||
|
||||
assertEquals(List.of("evacuate", "unload", "close-generator", "delete-folders"), phases);
|
||||
configurator.verifyNoInteractions();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
package art.arcane.iris.studio.view;
|
||||
|
||||
import art.arcane.iris.configuration.IrisSettings;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.awt.AWTEvent;
|
||||
import java.awt.EventQueue;
|
||||
import java.awt.Toolkit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.junit.Assert.assertNull;
|
||||
|
||||
public class DesktopGuiLaunchTest {
|
||||
private IrisSettings previousSettings;
|
||||
private boolean previousSuppressed;
|
||||
private RecordingEventQueue events;
|
||||
|
||||
@Before
|
||||
public void suppressDesktop() throws Exception {
|
||||
previousSettings = IrisSettings.settings;
|
||||
IrisSettings.settings = new IrisSettings();
|
||||
previousSuppressed = GuiHost.isDesktopSuppressed();
|
||||
GuiHost.suppressDesktop(true);
|
||||
events = new RecordingEventQueue();
|
||||
Toolkit.getDefaultToolkit().getSystemEventQueue().push(events);
|
||||
EventQueue.invokeAndWait(() -> {
|
||||
});
|
||||
}
|
||||
|
||||
@After
|
||||
public void restoreDesktop() throws Exception {
|
||||
EventQueue.invokeAndWait(events::remove);
|
||||
GuiHost.suppressDesktop(previousSuppressed);
|
||||
IrisSettings.settings = previousSettings;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mapLaunchDoesNotCreateAWindowWithoutDesktop() throws Exception {
|
||||
VisionGUI.launch(null, null);
|
||||
|
||||
assertNoDesktopFailure();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void noiseLaunchDoesNotCreateAWindowWithoutDesktop() throws Exception {
|
||||
NoiseExplorerGUI.launch();
|
||||
|
||||
assertNoDesktopFailure();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void generatorLaunchDoesNotCreateAWindowWithoutDesktop() throws Exception {
|
||||
NoiseExplorerGUI.launchGeneratorKey("test", null, 12345L);
|
||||
|
||||
assertNoDesktopFailure();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void imageMapLaunchDoesNotReadEngineWithoutDesktop() throws Exception {
|
||||
ImageMapStudioGUI.launch(null);
|
||||
|
||||
assertNoDesktopFailure();
|
||||
}
|
||||
|
||||
private void assertNoDesktopFailure() throws Exception {
|
||||
EventQueue.invokeAndWait(() -> {
|
||||
});
|
||||
assertNull("Desktop launch raised an exception on the AWT event thread", events.failure.get());
|
||||
}
|
||||
|
||||
private static final class RecordingEventQueue extends EventQueue {
|
||||
private final AtomicReference<Throwable> failure = new AtomicReference<>();
|
||||
|
||||
@Override
|
||||
protected void dispatchEvent(AWTEvent event) {
|
||||
try {
|
||||
super.dispatchEvent(event);
|
||||
} catch (Throwable error) {
|
||||
failure.compareAndSet(null, error);
|
||||
}
|
||||
}
|
||||
|
||||
private void remove() {
|
||||
pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package art.arcane.iris.world.lifecycle;
|
||||
|
||||
import art.arcane.iris.pack.datapack.ServerConfigurator;
|
||||
import art.arcane.iris.world.IrisToolbelt;
|
||||
import art.arcane.volmlib.util.bukkit.WorldIdentity;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.NamespacedKey;
|
||||
@@ -10,9 +12,13 @@ import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.CompletionStage;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
@@ -22,6 +28,7 @@ import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.verify;
|
||||
@@ -237,6 +244,96 @@ public class WorldLifecycleUnloadAsyncTest {
|
||||
assertEquals("remembered", service.backendNameForWorld(worldKey));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void settledUnloadRetainsTheBoundaryUntilBackendCompletionWithoutATerminalTimer() {
|
||||
ControlledBackend remembered = new ControlledBackend("remembered");
|
||||
WorldLifecycleService service = service(remembered, new ControlledBackend("fallback"));
|
||||
NamespacedKey worldKey = new NamespacedKey("iris", "settled_boundary");
|
||||
World world = world(worldKey);
|
||||
service.rememberBackend(worldKey, remembered.backendName());
|
||||
ArrayList<Runnable> timeouts = new ArrayList<>();
|
||||
Executor captureTimeout = timeouts::add;
|
||||
|
||||
try (MockedStatic<CompletableFuture> futures = mockStatic(CompletableFuture.class, CALLS_REAL_METHODS)) {
|
||||
futures.when(() -> CompletableFuture.delayedExecutor(120L, TimeUnit.SECONDS))
|
||||
.thenReturn(captureTimeout);
|
||||
|
||||
CompletableFuture<Boolean> result = service.unloadSettledAsync(world, false);
|
||||
CompletionStage<Boolean> boundary = WorldUnloadBoundaryRegistry.claim(worldKey.toString());
|
||||
|
||||
assertNotNull(boundary);
|
||||
assertFalse(boundary.toCompletableFuture().isDone());
|
||||
assertFalse(result.isDone());
|
||||
assertTrue(timeouts.isEmpty());
|
||||
assertEquals("remembered", service.backendNameForWorld(worldKey));
|
||||
|
||||
remembered.complete(true);
|
||||
|
||||
assertTrue(result.join());
|
||||
assertTrue(boundary.toCompletableFuture().join());
|
||||
assertEquals("fallback", service.backendNameForWorld(worldKey));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cancellingSettledUnloadResultPreservesBackendCompletionBookkeeping() {
|
||||
ControlledBackend remembered = new ControlledBackend("remembered");
|
||||
WorldLifecycleService service = service(remembered, new ControlledBackend("fallback"));
|
||||
NamespacedKey worldKey = new NamespacedKey("iris", "cancelled_settlement");
|
||||
World world = world(worldKey);
|
||||
service.rememberBackend(worldKey, remembered.backendName());
|
||||
|
||||
CompletableFuture<Boolean> result = service.unloadSettledAsync(world, false);
|
||||
CompletionStage<Boolean> boundary = WorldUnloadBoundaryRegistry.claim(worldKey.toString());
|
||||
|
||||
assertNotNull(boundary);
|
||||
assertTrue(result.cancel(false));
|
||||
assertFalse(boundary.toCompletableFuture().isDone());
|
||||
assertEquals("remembered", service.backendNameForWorld(worldKey));
|
||||
|
||||
remembered.complete(true);
|
||||
|
||||
assertTrue(result.isCancelled());
|
||||
assertTrue(boundary.toCompletableFuture().join());
|
||||
assertEquals("fallback", service.backendNameForWorld(worldKey));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void guardedUnloadKeepsItsTimeoutAndTracksLateBackendSuccess() {
|
||||
ControlledBackend remembered = new ControlledBackend("remembered");
|
||||
WorldLifecycleService service = service(remembered, new ControlledBackend("fallback"));
|
||||
NamespacedKey worldKey = new NamespacedKey("iris", "late_settlement");
|
||||
World world = world(worldKey);
|
||||
service.rememberBackend(worldKey, remembered.backendName());
|
||||
ArrayList<Runnable> timeouts = new ArrayList<>();
|
||||
Executor captureTimeout = timeouts::add;
|
||||
|
||||
try (MockedStatic<CompletableFuture> futures = mockStatic(CompletableFuture.class, CALLS_REAL_METHODS);
|
||||
MockedStatic<ServerConfigurator> configurator = mockStatic(ServerConfigurator.class);
|
||||
MockedStatic<IrisToolbelt> toolbelt = mockStatic(IrisToolbelt.class)) {
|
||||
futures.when(() -> CompletableFuture.delayedExecutor(120L, TimeUnit.SECONDS))
|
||||
.thenReturn(captureTimeout);
|
||||
|
||||
CompletableFuture<Boolean> result = service.unloadAsync(world, false);
|
||||
CompletionStage<Boolean> boundary = WorldUnloadBoundaryRegistry.claim(worldKey.toString());
|
||||
|
||||
assertEquals(1, timeouts.size());
|
||||
timeouts.getFirst().run();
|
||||
|
||||
CompletionException failure = assertThrows(CompletionException.class, result::join);
|
||||
assertTrue(failure.getCause() instanceof TimeoutException);
|
||||
configurator.verify(() -> ServerConfigurator.restart("World unload timed out for \"late_settlement\"."));
|
||||
assertFalse(boundary.toCompletableFuture().isDone());
|
||||
assertEquals("remembered", service.backendNameForWorld(worldKey));
|
||||
|
||||
remembered.complete(true);
|
||||
|
||||
assertTrue(boundary.toCompletableFuture().join());
|
||||
assertTrue(result.isCompletedExceptionally());
|
||||
assertEquals("fallback", service.backendNameForWorld(worldKey));
|
||||
}
|
||||
}
|
||||
|
||||
private static WorldLifecycleService service(
|
||||
ControlledBackend remembered,
|
||||
ControlledBackend fallback
|
||||
|
||||
Reference in new issue
Block a user