This commit is contained in:
Brian Neumann-Fopiano committed 2026-08-24 02:27:26 -04:00
1 parent fe5651f854
commit e20bb86f40
228 files changed
+5716 -3577

No files matched your search

@@ -5,6 +5,8 @@ import org.junit.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.stream.Stream;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -42,15 +44,46 @@ public class DiagnosticSeverityPolicyTest {
assertLoggedWith("core/pregenerator/PregenMantleBackpressure.java", "Pregen mantle backpressure: ", "info");
assertLoggedWith("core/pregenerator/PregenMantleBackpressure.java", "Pregen heap pressure: pausing generation", "info");
assertLoggedWith("core/pregenerator/MantleHeapPressure.java", "Iris heap remained at", "info");
assertLoggedWith("core/pregenerator/methods/AsyncPregenMethod.java", "is still pending after", "info");
assertLoggedWith("core/pregenerator/methods/AsyncPregenMethod.java", "is still pending after", "debug");
}
@Test
public void probesOnOptionalDependenciesAreNotWarnings() {
public void generationTracingStaysAtDebugLevel() {
assertLoggedWith("engine/mantle/components/MantleObjectComponent.java", "Regen object layer start:", "debug");
assertLoggedWith("engine/mantle/components/MantleObjectComponent.java", "Goldendebug object attempt:", "debug");
assertLoggedWith("engine/mantle/components/GoldenDebugObjectPlacer.java", "Goldendebug query:", "debug");
assertLoggedWith("engine/mantle/components/IrisStructureComponent.java", "[StructTrace] ORIGIN", "debug");
}
@Test
public void coreProductionDoesNotWriteDirectlyToProcessStreams() throws IOException {
try (Stream<Path> files = Files.walk(Path.of("src/main/java"))) {
List<Path> bypasses = files
.filter(path -> path.toString().endsWith(".java"))
.filter(path -> {
try {
String source = Files.readString(path);
return source.contains("System.out")
|| source.contains("System.err")
|| source.contains(".printStackTrace();");
} catch (IOException unreadable) {
throw new IllegalStateException(unreadable);
}
})
.toList();
assertTrue(bypasses.toString(), bypasses.isEmpty());
}
}
@Test
public void probesOnOptionalDependenciesAreNotWarnings() throws IOException {
assertLoggedWith("core/link/MultiverseCoreLink.java", "is not reachable; Iris world settings", "info");
assertLoggedWith("core/link/MultiverseCoreLink.java", "Multiverse will record the live name", "info");
assertLoggedWith("core/gui/GuiHost.java", "Unable to install the Iris desktop quit guard", "info");
assertLoggedWith("util/common/misc/SlimJar.java", "Failed to inject the library loader", "info");
String slimJar = read("util/common/misc/SlimJar.java");
assertTrue(slimJar.contains("debug(plugin, \"Failed to inject the library loader"));
assertTrue(slimJar.contains("if (DEBUG)"));
assertTrue(slimJar.contains("plugin.getLogger().info(\"[DEBUG] \" + message)"));
}
/**
@@ -13,21 +13,23 @@ import static org.junit.Assert.assertThrows;
public class IrisSettingsDefaultsTest {
@Test
public void concurrencyIsNotSerializedIntoSettingsJson() {
public void concurrencyIsNotSerializedIntoIrisJson() {
String json = new Gson().toJson(new IrisSettings());
assertFalse("settings.json must not advertise an unconfigurable concurrency block",
assertFalse("iris.json must not advertise an unconfigurable concurrency block",
json.contains("\"concurrency\""));
}
@Test
public void legacyConcurrencyBlockStillLoadsAndKeepsDerivedValues() {
IrisSettings loaded = new Gson().fromJson(
"{\"concurrency\":{\"parallelism\":8,\"ioParallelism\":4}}", IrisSettings.class);
public void languageAndMetricsAreTheFirstGeneralControls() {
String json = new Gson().toJson(new IrisSettings());
assertNotNull(loaded.getConcurrency());
assertEquals(Math.max(2, Runtime.getRuntime().availableProcessors()),
loaded.getConcurrency().getParallelism());
int language = json.indexOf("\"language\"");
int metrics = json.indexOf("\"metrics\"");
int commandSounds = json.indexOf("\"commandSounds\"");
assertTrue(language >= 0);
assertTrue(language < metrics);
assertTrue(metrics < commandSounds);
}
@Test
@@ -34,8 +34,12 @@ public class ProgressBossBarToggleTest {
}
@Test
public void worldCreationBossBarIsGatedBySettings() throws Exception {
assertGated("core/tools/WorldCreationProgressReporter.java");
public void worldCreationAlwaysUsesTheActionBarInsteadOfABossBar() throws Exception {
String source = source("core/tools/WorldCreationProgressReporter.java");
assertFalse(source.contains("isProgressBossBar()"));
assertFalse(source.contains("Bukkit.createBossBar("));
assertTrue(source.contains("WORLD_CREATE_LIFECYCLE_ACTION"));
}
@Test
@@ -4,9 +4,14 @@ import art.arcane.iris.spi.IrisLogging;
import org.bukkit.Bukkit;
import org.junit.Test;
import org.mockito.MockedStatic;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@@ -45,6 +50,48 @@ public class ServerConfiguratorStartupRestartTest {
}
}
@Test
public void immediateRestartCapabilityIsOptional() throws ReflectiveOperationException {
RestartCapableApi.restarted = false;
assertTrue(ServerConfigurator.invokeImmediateRestartIfSupported(RestartCapableApi.class));
assertTrue(RestartCapableApi.restarted);
assertFalse(ServerConfigurator.invokeImmediateRestartIfSupported(ShutdownOnlyApi.class));
}
@Test
public void productionBytecodeDoesNotLinkBukkitRestartDirectly() throws Exception {
AtomicBoolean directRestartInvocation = new AtomicBoolean(false);
ClassReader reader = new ClassReader(ServerConfigurator.class.getName());
reader.accept(new ClassVisitor(Opcodes.ASM9) {
@Override
public MethodVisitor visitMethod(
int access,
String name,
String descriptor,
String signature,
String[] exceptions
) {
return new MethodVisitor(Opcodes.ASM9) {
@Override
public void visitMethodInsn(
int opcode,
String owner,
String methodName,
String methodDescriptor,
boolean isInterface
) {
if ("org/bukkit/Bukkit".equals(owner) && "restart".equals(methodName)) {
directRestartInvocation.set(true);
}
}
};
}
}, ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
assertFalse(directRestartInvocation.get());
}
@Test
public void startupRestartPathsPromoteTheValidationStateAndBypassTickQueues() throws Exception {
String source = Files.readString(Path.of(
@@ -62,12 +109,24 @@ public class ServerConfiguratorStartupRestartTest {
int validationRestart = configure.indexOf("requireDatapackRestart();", restartResult);
assertTrue(restartResult >= 0);
assertTrue(validationRestart > restartResult);
assertTrue(startupRestart.indexOf("Bukkit.restart();")
assertTrue(startupRestart.indexOf("invokeImmediateRestartIfSupported(Bukkit.class)")
< startupRestart.indexOf("Bukkit.shutdown();"));
assertFalse(startupRestart.contains("Bukkit.restart"));
assertFalse(startupRestart.contains("J.s("));
assertFalse(startupRestart.contains("dispatchCommand"));
}
public static final class RestartCapableApi {
private static boolean restarted;
public static void restart() {
restarted = true;
}
}
public static final class ShutdownOnlyApi {
}
private static String section(String source, String startMarker, String endMarker) {
int start = source.indexOf(startMarker);
int end = source.indexOf(endMarker, start);
@@ -43,7 +43,7 @@ public class SettingsHotloadWatchTest {
public void setUp() throws Exception {
previousSettings = IrisSettings.settings;
dataFolder = temporaryFolder.newFolder("iris-hotload");
settingsFile = new File(dataFolder, "settings.json");
settingsFile = new File(dataFolder, "iris.json");
overrideFolder = new File(dataFolder, "languages/overrides");
Files.createDirectories(overrideFolder.toPath());
String settings = settings("en_US");
@@ -25,6 +25,13 @@ public class VisionRenderTypeTest {
assertEquals("iris.desktop.vision.mode.river", DesktopUiMessages.VISION_MODE_RIVER.id());
}
@Test
public void teleportCoordinatesFloorAcrossTheNegativeOrigin() {
assertEquals(-1, VisionGUI.floorWorldCoordinate(-0.01D));
assertEquals(0, VisionGUI.floorWorldCoordinate(0.99D));
assertEquals(1, VisionGUI.floorWorldCoordinate(1D));
}
private static void assertTrueCatalogMember(MessageKey key) {
assertEquals(key.id(), 1, DesktopUiMessages.keys().stream()
.filter(candidate -> candidate.id().equals(key.id()))
@@ -84,11 +84,11 @@ public class WorldLifecycleSelectionTest {
}
@Test
public void persistentCreateFallsBackToBukkitBackendOnFoliaWithoutRuntime() {
WorldLifecycleService service = new WorldLifecycleService(CapabilitySnapshot.forTesting(ServerFamily.FOLIA, true, false, false));
public void persistentCreateFailsClosedOnFoliaWithoutRuntime() {
WorldLifecycleService service = new WorldLifecycleService(CapabilitySnapshot.forTesting(ServerFamily.FOLIA, false, false, false));
WorldLifecycleRequest request = new WorldLifecycleRequest("persistent", NamespacedKey.minecraft("persistent"), World.Environment.NORMAL, null, null, null, true, false, 1337L, false, false, WorldLifecycleCaller.CREATE);
assertEquals("bukkit_public", service.selectCreateBackend(request).backendName());
assertThrows(IllegalStateException.class, () -> service.selectCreateBackend(request));
}
@Test
@@ -10,6 +10,8 @@ import org.junit.Test;
import org.mockito.MockedStatic;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.CompletionStage;
@@ -57,6 +59,40 @@ public class WorldLifecycleUnloadAsyncTest {
}
}
@Test
public void paperLikeManualUnloadDetachesBeforeOffThreadChunkDrain() throws Exception {
String source = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/lifecycle/WorldLifecycleSupport.java"))
.replace("\r\n", "\n");
int unloadStart = source.indexOf("private static CompletableFuture<Boolean> unloadWorldWithoutAsyncApi(");
int unloadEnd = source.indexOf("static boolean announceManualWorldUnload", unloadStart);
String unloadMethod = source.substring(unloadStart, unloadEnd);
int capabilityFallback = unloadMethod.indexOf(
"capabilities.minecraftServer() == null || capabilities.removeLevelMethod() == null");
int bukkitUnload = unloadMethod.indexOf("Bukkit.unloadWorld(world, save)", capabilityFallback);
int detach = unloadMethod.indexOf("detachServerLevelAsync(capabilities, serverLevel, world)");
int drain = unloadMethod.indexOf("drainChunkTasksAsync(world, serverLevel)");
int close = unloadMethod.indexOf("closeServerLevelAsync(world, serverLevel)");
int closeStart = source.indexOf("private static CompletableFuture<Void> closeServerLevelAsync(");
int closeEnd = source.indexOf("private static CompletableFuture<Void> drainChunkTasksAsync(", closeStart);
String closeMethod = source.substring(closeStart, closeEnd);
int drainStart = closeEnd;
int drainEnd = source.indexOf("private static void removeWorldFromCraftServerMap", drainStart);
String drainMethod = source.substring(drainStart, drainEnd);
assertTrue(capabilityFallback >= 0);
assertTrue(bukkitUnload > capabilityFallback);
assertTrue(detach > bukkitUnload);
assertTrue(drain > detach);
assertTrue(close > drain);
assertTrue(drainMethod.contains("return J.afut(() ->"));
assertTrue(drainMethod.contains("\"moonrise$getChunkTaskScheduler\""));
assertTrue(drainMethod.contains("\"halt\""));
assertTrue(closeMethod.contains("return runGlobalAsync(closeTask)"));
assertFalse(closeMethod.contains("J.afut("));
assertFalse(closeMethod.contains("J.runRegion("));
}
@Test
public void reflectedAsyncUnloadWaitsForTrueCallback() throws Exception {
CallbackServer server = new CallbackServer();
@@ -101,6 +137,49 @@ public class WorldLifecycleUnloadAsyncTest {
assertFalse(result.join());
}
@Test
public void reflectedAsyncUnloadAcceptsCanvasResultCallback() throws Exception {
CanvasCallbackServer server = new CanvasCallbackServer();
Method unloadMethod = CanvasCallbackServer.class.getMethod(
"unloadWorldAsync",
World.class,
boolean.class,
Consumer.class
);
CompletableFuture<Boolean> result = WorldLifecycleSupport.invokeAsyncUnload(
server,
unloadMethod,
mock(World.class),
true
);
assertFalse(result.isDone());
server.complete(CanvasUnloadResult.SUCCESS);
assertTrue(result.join());
}
@Test
public void reflectedAsyncUnloadMapsCanvasFailureResultToFalse() throws Exception {
CanvasCallbackServer server = new CanvasCallbackServer();
Method unloadMethod = CanvasCallbackServer.class.getMethod(
"unloadWorldAsync",
World.class,
boolean.class,
Consumer.class
);
CompletableFuture<Boolean> result = WorldLifecycleSupport.invokeAsyncUnload(
server,
unloadMethod,
mock(World.class),
false
);
server.complete(CanvasUnloadResult.FAILURE);
assertFalse(result.join());
}
@Test
public void reflectedAsyncUnloadPropagatesInvocationFailure() throws Exception {
FailingServer server = new FailingServer();
@@ -232,6 +311,27 @@ public class WorldLifecycleUnloadAsyncTest {
}
}
public static final class CanvasCallbackServer {
private Consumer<CanvasUnloadResult> callback;
public void unloadWorldAsync(World world, boolean save, Consumer<CanvasUnloadResult> callback) {
this.callback = callback;
}
private void complete(CanvasUnloadResult result) {
callback.accept(result);
}
}
public enum CanvasUnloadResult {
SUCCESS,
FAILURE;
public boolean isSuccess() {
return this == SUCCESS;
}
}
private static final class ControlledBackend implements WorldLifecycleBackend {
private final String name;
private final CompletableFuture<Boolean> unloadResult = new CompletableFuture<>();
@@ -79,12 +79,27 @@ public class PackRiverValidatorTest {
"minimumSourcesPerTile": 2,
"sinkSearchReaches": 8,
"routingBasinCells": 7,
"routingDeviationScaleCells": 7,
"routingDeviationStrengthCells": 33,
"routingPlateauHeight": 0,
"flowAlignmentWeight": 1025,
"confluenceWeight": -1,
"branchSoftCap": 9,
"branchChildShrinkFactor": 2,
"routingStyle": {"zoom": 0}
},
"terrain": {
"channelWidth": {"min": 40, "max": 12},
"depth": {}
"depth": {},
"tunnelWidthMultiplier": {"min": 0.5, "max": 9},
"tunnelMouthBlend": 17,
"tunnelFloorVariation": 9,
"tunnelRoofVariation": 17,
"tunnelFloorStyle": {"zoom": 0},
"tunnelRoofStyle": {"zoom": 0}
},
"caves": {
"parentBiomeInheritance": 2
}
}
}
@@ -96,10 +111,24 @@ public class PackRiverValidatorTest {
assertContains(result.errors(), "rivers.topology.minimumSourcesPerTile must not exceed tileCells squared.");
assertContains(result.errors(), "rivers.topology.sinkSearchReaches must be at most 7");
assertContains(result.errors(), "rivers.topology.routingBasinCells must be at least 8");
assertContains(result.errors(), "rivers.topology.routingDeviationScaleCells must be at least 8");
assertContains(result.errors(), "rivers.topology.routingDeviationStrengthCells must be at most 32");
assertContains(result.errors(), "rivers.topology.routingPlateauHeight must be at least 1");
assertContains(result.errors(), "rivers.topology.flowAlignmentWeight must be at most 1024");
assertContains(result.errors(), "rivers.topology.confluenceWeight must be at least 0");
assertContains(result.errors(), "rivers.topology.branchSoftCap must be at most 8");
assertContains(result.errors(), "rivers.topology.branchChildShrinkFactor must be at most 1");
assertContains(result.errors(), "rivers.topology.routingStyle.zoom must be at least");
assertContains(result.errors(), "rivers.terrain.channelWidth.min must not exceed");
assertContains(result.errors(), "rivers.terrain.depth must set min and max explicitly.");
assertContains(result.errors(), "rivers.terrain.tunnelWidthMultiplier.min must be at least 1");
assertContains(result.errors(), "rivers.terrain.tunnelWidthMultiplier.max must be at most 8");
assertContains(result.errors(), "rivers.terrain.tunnelMouthBlend must be at most 16");
assertContains(result.errors(), "rivers.terrain.tunnelFloorVariation must be at most 8");
assertContains(result.errors(), "rivers.terrain.tunnelRoofVariation must be at most 16");
assertContains(result.errors(), "rivers.terrain.tunnelFloorStyle.zoom must be at least");
assertContains(result.errors(), "rivers.terrain.tunnelRoofStyle.zoom must be at least");
assertContains(result.errors(), "rivers.caves.parentBiomeInheritance must be at most 1");
}
@Test
@@ -177,6 +206,27 @@ public class PackRiverValidatorTest {
assertContains(result.errors(), "rivers.terrain.maxDepth");
}
@Test
public void rejectsTunnelFootprintsAboveDerivedHydrologyBudget() throws Exception {
File pack = pack("""
{
"regions": ["region"],
"rivers": {
"enabled": true,
"terrain": {
"maxChannelWidth": 2048,
"tunnelMouthBlend": 16
}
}
}
""");
PackRiverValidator.Validation result = validate(pack);
assertContains(result.errors(), "exceeds the safe derived hydrology budget");
assertContains(result.errors(), "columns per generated chunk");
}
@Test
public void rejectsMissingBiomeReferencesAndWarnsAboutInferredRoles() throws Exception {
File pack = pack("""
@@ -199,6 +249,44 @@ public class PackRiverValidatorTest {
assertContains(result.warnings(), "inferred river role SHORE");
}
@Test
public void netherRiverBiomesKeepNetherDerivativesWithoutOverworldRoleWarnings() throws Exception {
File pack = pack("""
{
"environment": "NETHER",
"regions": ["region"],
"rivers": {
"enabled": true,
"biomes": {
"channel": ["biome"],
"bank": ["biome"]
}
}
}
""");
write(pack, "biomes/biome.json", """
{
"name": "Nether River",
"derivative": "minecraft:basalt_deltas",
"vanillaDerivative": "minecraft:basalt_deltas"
}
""");
write(pack, "regions/region.json", """
{
"landBiomes": ["biome"],
"riverOverride": {
"channelBiomes": ["biome"],
"bankBiomes": ["biome"]
}
}
""");
PackRiverValidator.Validation result = validate(pack);
assertFalse(result.errors().toString(), contains(result.errors(), "biome"));
assertFalse(result.warnings().toString(), contains(result.warnings(), "inferred river role"));
}
@Test
public void validatesRegionAndBiomeOverridesWhenRiversAreEnabled() throws Exception {
File pack = pack("""
@@ -0,0 +1,140 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.protocol;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.spi.protocol.IrisProtocol;
import org.junit.Test;
import java.util.ArrayDeque;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class IrisCursorRequestServiceTest {
private static final Executor DIRECT = Runnable::run;
@Test
public void burstCoalescesToLatestRequestPerSessionAndOneDrain() {
IrisSessionRegistry registry = new IrisSessionRegistry();
registerReady(registry, "s1");
QueuedExecutor executor = new QueuedExecutor();
IrisCursorRequestService service = new IrisCursorRequestService(
sessionId -> {
throw new AssertionError("queued work must not run early");
},
registry,
executor,
8);
for (int index = 0; index < 10; index++) {
service.handle("s1", index, -index);
}
assertEquals(1, service.pendingSize());
assertEquals(9L, service.coalescedCount());
assertEquals(1, executor.size());
}
@Test
public void saturationDropsOldestSessionsButKeepsQueueBounded() {
IrisSessionRegistry registry = new IrisSessionRegistry();
QueuedExecutor executor = new QueuedExecutor();
IrisCursorRequestService service = new IrisCursorRequestService(sessionId -> null, registry, executor, 2);
service.handle("s1", 1, 1);
service.handle("s2", 2, 2);
service.handle("s3", 3, 3);
assertEquals(2, service.pendingSize());
assertEquals(1L, service.droppedSaturatedCount());
assertEquals(1, executor.size());
}
@Test
public void directDrainResolvesAndSendsCursorResponse() {
IrisSessionRegistry registry = new IrisSessionRegistry();
CountingTransport transport = registerReady(registry, "s1");
Engine engine = mock(Engine.class);
IrisDimension dimension = mock(IrisDimension.class);
when(engine.isClosed()).thenReturn(false);
when(engine.getSurfaceBiome(4, 8)).thenReturn(null);
when(engine.getRegion(4, 8)).thenReturn(null);
when(engine.getCaveBiome(4, 8)).thenReturn(null);
when(engine.getMinHeight()).thenReturn(-64);
when(engine.getHeight(4, 8)).thenReturn(80);
when(engine.getDimension()).thenReturn(dimension);
when(dimension.getLoadKey()).thenReturn("overworld");
IrisCursorRequestService service = new IrisCursorRequestService(sessionId -> engine, registry, DIRECT, 8);
service.handle("s1", 4, 8);
assertEquals(1L, service.resolvedCount());
assertEquals(1, transport.sent.get());
assertEquals(0, service.pendingSize());
}
@Test
public void blankSessionIsRejectedWithoutSchedulingWork() {
IrisSessionRegistry registry = new IrisSessionRegistry();
QueuedExecutor executor = new QueuedExecutor();
IrisCursorRequestService service = new IrisCursorRequestService(sessionId -> null, registry, executor, 8);
service.handle(null, 0, 0);
service.handle("", 0, 0);
assertEquals(2L, service.droppedNoSessionCount());
assertEquals(0, service.pendingSize());
assertEquals(0, executor.size());
}
private static CountingTransport registerReady(IrisSessionRegistry registry, String sessionId) {
CountingTransport transport = new CountingTransport();
IrisSession session = new IrisSession(sessionId, transport);
session.markReady(IrisProtocol.PROTOCOL_VERSION, IrisProtocol.CAPABILITY_CURSOR);
registry.register(session);
return transport;
}
private static final class CountingTransport implements IrisServerTransport {
private final AtomicInteger sent = new AtomicInteger(0);
@Override
public void sendToClient(String sessionId, byte[] frame) {
sent.incrementAndGet();
}
}
private static final class QueuedExecutor implements Executor {
private final ArrayDeque<Runnable> tasks = new ArrayDeque<>();
@Override
public void execute(Runnable command) {
tasks.addLast(command);
}
private int size() {
return tasks.size();
}
}
}
@@ -350,6 +350,28 @@ public class IrisProtocolServerTest {
assertEquals(1L, server.visionTileForwardedCount());
}
@Test
public void visionTileOutsideWorldBoundsIsRejectedBeforeRendering() {
RecordingTransport transport = new RecordingTransport();
IrisSessionRegistry registry = new IrisSessionRegistry();
IrisProtocolServer server = new IrisProtocolServer(registry, SERVER_CAPABILITIES, BRAND, true);
IrisSession session = new IrisSession("s1", transport);
registry.register(session);
server.onClientFrame("s1", IrisMessageCodec.encode(new IrisMessage.ClientHello(
IrisProtocol.PROTOCOL_VERSION,
IrisProtocol.CAPABILITY_VISION)));
RecordingVisionHandler handler = new RecordingVisionHandler();
server.setVisionTileHandler(handler);
server.onClientFrame("s1", IrisMessageCodec.encode(new IrisMessage.VisionTileRequest(Integer.MAX_VALUE, 0, 0)));
server.onClientFrame("s1", IrisMessageCodec.encode(new IrisMessage.VisionTileRequest(0, Integer.MIN_VALUE, 8)));
server.onClientFrame("s1", IrisMessageCodec.encode(new IrisMessage.VisionTileRequest(0, 0, Integer.MAX_VALUE)));
assertEquals(1, handler.count);
assertEquals(2L, server.visionOutOfBoundsCount());
assertEquals(1L, server.visionTileForwardedCount());
}
@Test
public void visionTileRequestBudgetDropsExcessWithinWindow() {
RecordingTransport transport = new RecordingTransport();
@@ -21,6 +21,9 @@ package art.arcane.iris.core.protocol;
import art.arcane.iris.spi.protocol.IrisProtocol;
import org.junit.Test;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicInteger;
@@ -50,6 +53,94 @@ public class IrisVisionRequestServiceTest {
assertEquals(maxPending, service.pendingSize());
}
@Test
public void burstUsesAtMostTwoDrainsAndDrainsEveryRetainedRequest() {
IrisSessionRegistry registry = new IrisSessionRegistry();
registerReady(registry, "s1", IrisProtocol.CAPABILITY_VISION);
EngineResolver resolver = sessionId -> null;
QueuedExecutor executor = new QueuedExecutor();
int maxPending = 4;
IrisVisionRequestService service = new IrisVisionRequestService(resolver, registry, executor, maxPending);
for (int index = 0; index < 10; index++) {
service.handle("s1", index, 0, 0);
}
assertEquals(2, executor.size());
assertEquals(maxPending, service.pendingSize());
executor.runAll();
assertEquals(0, service.pendingSize());
assertEquals(maxPending, service.droppedNoEngineCount());
}
@Test
public void duplicateTileRequestsCoalesceWithoutConsumingCapacity() {
IrisSessionRegistry registry = new IrisSessionRegistry();
registerReady(registry, "s1", IrisProtocol.CAPABILITY_VISION);
IrisVisionRequestService service = new IrisVisionRequestService(sessionId -> null, registry, DISABLED, 8);
service.handle("s1", 4, -7, 2);
service.handle("s1", 4, -7, 2);
service.handle("s1", 4, -7, 2);
assertEquals(1, service.pendingSize());
assertEquals(0L, service.droppedSaturatedCount());
}
@Test
public void drainRotatesAcrossSessionsInsteadOfServingOneBurstFirst() {
IrisSessionRegistry registry = new IrisSessionRegistry();
registerReady(registry, "s1", IrisProtocol.CAPABILITY_VISION);
registerReady(registry, "s2", IrisProtocol.CAPABILITY_VISION);
ArrayList<String> resolutionOrder = new ArrayList<>();
EngineResolver resolver = sessionId -> {
resolutionOrder.add(sessionId);
return null;
};
QueuedExecutor executor = new QueuedExecutor();
IrisVisionRequestService service = new IrisVisionRequestService(resolver, registry, executor, 8);
service.handle("s1", 0, 0, 0);
service.handle("s1", 1, 0, 0);
service.handle("s2", 0, 0, 0);
service.handle("s2", 1, 0, 0);
executor.runAll();
assertEquals(List.of("s1", "s2", "s1", "s2"), resolutionOrder);
}
@Test
public void saturatedSessionCannotExcludeAnotherSession() {
IrisSessionRegistry registry = new IrisSessionRegistry();
registerReady(registry, "noisy", IrisProtocol.CAPABILITY_VISION);
registerReady(registry, "other", IrisProtocol.CAPABILITY_VISION);
IrisVisionRequestService service = new IrisVisionRequestService(sessionId -> null, registry, DISABLED, 4);
for (int tile = 0; tile < 20; tile++) {
service.handle("noisy", tile, 0, 0);
}
service.handle("other", 0, 0, 0);
assertEquals(4, service.pendingSize());
service.clearSession("noisy");
assertEquals(1, service.pendingSize());
}
@Test
public void blankSessionIsRejectedWithoutSchedulingWork() {
IrisSessionRegistry registry = new IrisSessionRegistry();
QueuedExecutor executor = new QueuedExecutor();
IrisVisionRequestService service = new IrisVisionRequestService(sessionId -> null, registry, executor, 8);
service.handle(null, 0, 0, 0);
service.handle("", 0, 0, 0);
service.handle(" ", 0, 0, 0);
assertEquals(3L, service.droppedNoSessionCount());
assertEquals(0, service.pendingSize());
assertEquals(0, executor.size());
}
@Test
public void nullEngineDropsAndCounts() {
IrisSessionRegistry registry = new IrisSessionRegistry();
@@ -157,4 +248,27 @@ public class IrisVisionRequestServiceTest {
sent.incrementAndGet();
}
}
private static final class QueuedExecutor implements Executor {
private final ArrayDeque<Runnable> tasks = new ArrayDeque<>();
@Override
public void execute(Runnable command) {
tasks.addLast(command);
}
private int size() {
return tasks.size();
}
private void runNext() {
tasks.removeFirst().run();
}
private void runAll() {
while (!tasks.isEmpty()) {
runNext();
}
}
}
}
@@ -36,12 +36,12 @@ public class StudioEntitySpawningTest {
}
@Test
public void generalStudioUsesAPlayerModeEligibleForNaturalSpawning() throws IOException {
public void standardStudioDefaultsToSpectatorMode() throws IOException {
String source = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/project/IrisProject.java")).replace("\r\n", "\n");
"src/main/java/art/arcane/iris/core/runtime/StudioOpenCoordinator.java")).replace("\r\n", "\n");
assertFalse(source.contains("GameMode.SPECTATOR"));
assertTrue(source.contains("GameMode.CREATIVE"));
assertTrue(source.contains("request.openKind().teleportThroughStandardEntry()"));
assertTrue(source.contains("GameMode.SPECTATOR"));
}
@Test
@@ -1,19 +1,19 @@
package art.arcane.iris.core.runtime;
import art.arcane.iris.core.ServerConfigurator;
import org.junit.Test;
import org.mockito.MockedStatic;
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.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.mockStatic;
public class StudioOpenCoordinatorCloseSequenceTest {
@Test
@@ -104,126 +104,22 @@ public class StudioOpenCoordinatorCloseSequenceTest {
}
@Test
public void pendingEntryLoadDefersCleanupUntilSettlement() {
CompletableFuture<Void> entryLoad = new CompletableFuture<>();
AtomicInteger cleanups = new AtomicInteger(0);
public void closeFailureAfterUnloadBeginsRequestsTerminalRestart() {
try (MockedStatic<ServerConfigurator> configurator = mockStatic(ServerConfigurator.class)) {
StudioOpenCoordinator.requestRestartAfterPartialClose("studio-world", true);
CompletableFuture<Void> deferred = StudioOpenCoordinator.deferCleanupUntilEntrySettlement(
entryLoad,
() -> {
cleanups.incrementAndGet();
return CompletableFuture.completedFuture(null);
});
assertEquals(0, cleanups.get());
assertFalse(deferred.isDone());
entryLoad.complete(null);
deferred.join();
assertEquals(1, cleanups.get());
entryLoad.complete(null);
assertEquals(1, cleanups.get());
}
@Test
public void onlyAnUnsettledEntryLoadRequiresDeferredCleanup() {
CompletableFuture<Void> pending = new CompletableFuture<>();
CompletableFuture<Void> failed = CompletableFuture.failedFuture(
new IllegalStateException("entry failed"));
assertTrue(StudioOpenCoordinator.requiresDeferredEntryCleanup(pending));
assertFalse(StudioOpenCoordinator.requiresDeferredEntryCleanup(
CompletableFuture.completedFuture(null)));
assertFalse(StudioOpenCoordinator.requiresDeferredEntryCleanup(failed));
assertFalse(StudioOpenCoordinator.requiresDeferredEntryCleanup(null));
}
@Test
public void retryRemainsFencedUntilDeferredCleanupCompletes() {
StudioOpenCoordinator.EntryLoadRegistry registry =
new StudioOpenCoordinator.EntryLoadRegistry();
CompletableFuture<Void> entryLoad = new CompletableFuture<>();
CompletableFuture<Void> cleanup = new CompletableFuture<>();
registry.register("iris-entry-timeout", entryLoad);
CompletableFuture<Void> deferred = StudioOpenCoordinator.deferCleanupUntilEntrySettlement(
entryLoad,
() -> cleanup);
registry.releaseAfterSuccessfulCompletion(
"iris-entry-timeout",
entryLoad,
deferred);
entryLoad.complete(null);
assertFalse(deferred.isDone());
assertTrue(registry.isFenced("iris-entry-timeout"));
try {
registry.rejectNewOpen();
fail("Expected the retry to be rejected");
} catch (IllegalStateException exception) {
assertTrue(exception.getMessage().contains("iris-entry-timeout"));
configurator.verify(() -> ServerConfigurator.restart(
"Studio close failed after world unload began for \"studio-world\"."));
}
cleanup.complete(null);
deferred.join();
assertFalse(registry.isFenced("iris-entry-timeout"));
registry.rejectNewOpen();
}
@Test
public void exceptionalEntrySettlementStillTriggersCleanupOnce() {
CompletableFuture<Void> entryLoad = new CompletableFuture<>();
AtomicInteger cleanups = new AtomicInteger(0);
public void closeFailureBeforeUnloadDoesNotRequestRestart() {
try (MockedStatic<ServerConfigurator> configurator = mockStatic(ServerConfigurator.class)) {
StudioOpenCoordinator.requestRestartAfterPartialClose("studio-world", false);
CompletableFuture<Void> deferred = StudioOpenCoordinator.deferCleanupUntilEntrySettlement(
entryLoad,
() -> {
cleanups.incrementAndGet();
return CompletableFuture.completedFuture(null);
});
entryLoad.completeExceptionally(new IllegalStateException("entry failed"));
deferred.join();
assertEquals(1, cleanups.get());
}
@Test
public void nonSettlingEntryLoadNeverStartsCleanup() {
CompletableFuture<Void> entryLoad = new CompletableFuture<>();
AtomicInteger cleanups = new AtomicInteger(0);
CompletableFuture<Void> deferred = StudioOpenCoordinator.deferCleanupUntilEntrySettlement(
entryLoad,
() -> {
cleanups.incrementAndGet();
return CompletableFuture.completedFuture(null);
});
assertFalse(deferred.isDone());
assertEquals(0, cleanups.get());
}
@Test
public void deferredCleanupFailureIsPreserved() {
StudioOpenCoordinator.EntryLoadRegistry registry =
new StudioOpenCoordinator.EntryLoadRegistry();
CompletableFuture<Void> entryLoad = new CompletableFuture<>();
IllegalStateException cleanupFailure = new IllegalStateException("cleanup failed");
registry.register("iris-cleanup-failure", entryLoad);
CompletableFuture<Void> deferred = StudioOpenCoordinator.deferCleanupUntilEntrySettlement(
entryLoad,
() -> CompletableFuture.failedFuture(cleanupFailure));
registry.releaseAfterSuccessfulCompletion(
"iris-cleanup-failure",
entryLoad,
deferred);
entryLoad.complete(null);
try {
deferred.join();
fail("Expected deferred cleanup failure");
} catch (CompletionException exception) {
assertSame(cleanupFailure, exception.getCause());
configurator.verifyNoInteractions();
}
assertTrue(registry.isFenced("iris-cleanup-failure"));
}
private static CompletableFuture<Void> phase(List<String> phases, String phase) {
@@ -27,7 +27,7 @@ public class StudioOpenCoordinatorOpenKindTest {
}
@Test
public void bothStudioKindsReuseOnlyAnUnchangedLoadedRuntime() {
public void allStudioKindsReuseOnlyAnUnchangedLoadedRuntime() {
for (StudioOpenCoordinator.StudioOpenKind kind : StudioOpenCoordinator.StudioOpenKind.values()) {
assertEquals(
IrisCreator.DatapackPreparation.REUSE_LOADED_RUNTIME_IF_READY,
@@ -35,6 +35,15 @@ public class StudioOpenCoordinatorOpenKindTest {
}
}
@Test
public void objectStudioOwnsWorkspaceWithoutUsingTheStandardEntry() {
StudioOpenCoordinator.StudioOpenKind kind = StudioOpenCoordinator.StudioOpenKind.OBJECT;
assertTrue(kind.openWorkspace());
assertFalse(kind.teleportThroughStandardEntry());
assertTrue(kind.prepareGeneratorState());
}
@Test
public void jigsawStudioLeavesWorkspaceClosedAndUsesItsDestinationTeleport() {
StudioOpenCoordinator.StudioOpenKind kind = StudioOpenCoordinator.StudioOpenKind.JIGSAW;
@@ -62,22 +71,6 @@ public class StudioOpenCoordinatorOpenKindTest {
assertEquals(StudioOpenCoordinator.StudioOpenKind.JIGSAW, request.openKind());
}
@Test
public void onlyAStandardPlayerOpenLoadsTheEntry() {
assertTrue(StudioOpenCoordinator.requiresLoadedEntry(request(
StudioOpenCoordinator.StudioOpenKind.STANDARD,
"Magic_Psycho")));
assertFalse(StudioOpenCoordinator.requiresLoadedEntry(request(
StudioOpenCoordinator.StudioOpenKind.STANDARD,
null)));
assertFalse(StudioOpenCoordinator.requiresLoadedEntry(request(
StudioOpenCoordinator.StudioOpenKind.STANDARD,
" ")));
assertFalse(StudioOpenCoordinator.requiresLoadedEntry(request(
StudioOpenCoordinator.StudioOpenKind.JIGSAW,
"Magic_Psycho")));
}
@Test
public void activeStudioTeleportIsSerializedAndBoundedBeforeNativeDelegation() throws Exception {
String coordinator = Files.readString(Path.of(
@@ -103,42 +96,57 @@ public class StudioOpenCoordinatorOpenKindTest {
int nativeClaim = coordinatorMethod.indexOf(
"activeAdmission.compareAndSet(true, false)");
int nativeDelegation = coordinatorMethod.indexOf(
"WorldRuntimeControlService.get().teleport(player, entry)");
"WorldRuntimeControlService.get().teleportInMode(player, entry, GameMode.SPECTATOR)");
assertTrue(transitionAdmission >= 0);
assertTrue(projectCapture > transitionAdmission);
assertTrue(publicDeadline > projectCapture);
assertTrue(admissionClose > publicDeadline);
assertTrue(serviceMethod.contains("deadlineNanos"));
assertTrue(coordinatorMethod.contains("beforeStudioTeleportDeadline("));
assertTrue(coordinatorMethod.contains("CompletableFuture<T> bounded = new CompletableFuture<>()"));
assertTrue(coordinatorMethod.contains(
"CompletableFuture.delayedExecutor(remainingNanos, TimeUnit.NANOSECONDS)"));
assertFalse(coordinatorMethod.contains("stage.orTimeout("));
assertTrue(coordinatorMethod.contains("EntryChunkResolution entryResolution"));
assertFalse(coordinatorMethod.contains("addPluginChunkTicket("));
assertFalse(coordinatorMethod.contains("removePluginChunkTicket("));
"WorldRuntimeControlService.get().resolveEntryAnchor(world, provider)"));
assertTrue(coordinatorMethod.contains(
"project.getActiveOpenKind() == StudioOpenKind.STANDARD"));
assertFalse(coordinatorMethod.contains("requestChunkAsync("));
assertFalse(coordinatorMethod.contains("getHighestBlockYAt("));
assertTrue(nativeClaim >= 0);
assertTrue(nativeDelegation > nativeClaim);
}
@Test
public void entryLoadUsesOnlyTheUrgentExactChunkRequest() throws Exception {
public void standardEntryDelegatesWithoutAnExplicitChunkOrSurfaceLookup() throws Exception {
String source = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/runtime/StudioOpenCoordinator.java"))
.replace("\r\n", "\n");
int loadStart = source.indexOf("private EntryChunkResolution loadEntryChunk(");
int loadEnd = source.indexOf("private void settleEntryUseAfterOperation(", loadStart);
String load = source.substring(loadStart, loadEnd);
assertTrue(load.contains("requestChunkAsync("));
assertTrue(load.contains("true,\n true"));
assertTrue(load.contains("J.isOwnedByCurrentRegion(world, chunkX, chunkZ)"));
assertTrue(load.contains("J.runRegion(world, chunkX, chunkZ"));
assertTrue(load.contains("WorldRuntimeControlService.findTopSafeStudioLocation(world, entryAnchor)"));
assertTrue(source.contains("Location entryLocation = entryAnchor;"));
assertFalse(source.contains("prepareStudioEntryChunks("));
assertFalse(source.contains("findStudioEntryLocation"));
assertFalse(source.contains("EntryChunkResolution"));
assertFalse(source.contains("resolveSafeEntry(world, entryAnchor)"));
assertFalse(source.contains("addPluginChunkTicket("));
assertFalse(source.contains("removePluginChunkTicket("));
assertFalse(source.contains("getHighestBlockYAt("));
}
@Test
public void standardEntryBindsSpectatorToTheNativeTeleport() throws Exception {
String source = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/runtime/StudioOpenCoordinator.java"))
.replace("\r\n", "\n");
String runtime = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/runtime/WorldRuntimeControlService.java"))
.replace("\r\n", "\n");
int mode = runtime.indexOf("modeRestore.apply(gameMode)");
int teleport = runtime.indexOf("teleporter.teleport(player, location)", mode);
assertTrue(source.contains("WorldRuntimeControlService.get().teleportInMode("));
int openKind = source.indexOf("request.project().setActiveOpenKind(request.openKind())");
int provider = source.indexOf("request.project().setActiveProvider(provider)", openKind);
assertTrue(openKind >= 0);
assertTrue(provider > openKind);
assertTrue(source.contains("project.setActiveOpenKind(null)"));
assertTrue(mode >= 0);
assertTrue(teleport > mode);
assertTrue(runtime.contains("modeRestore.restore()"));
}
@Test
@@ -153,9 +161,6 @@ public class StudioOpenCoordinatorOpenKindTest {
"prepare_generator",
"resolve_entry_anchor",
"prepare_structure_rings",
"prepare_entry_chunks",
"load_entry_chunk",
"resolve_safe_entry",
"teleport_standard_entry",
"finalize_open")) {
int current = source.indexOf("\"" + phase + "\"", previous + 1);
@@ -166,18 +171,16 @@ public class StudioOpenCoordinatorOpenKindTest {
}
@Test
public void structureStateActivatesBeforeTheRealEntryChunkLoads() throws Exception {
public void structureStateCompletesBeforeImmediateNativeTeleport() throws Exception {
String source = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/runtime/StudioOpenCoordinator.java")).replace("\r\n", "\n");
int completionCall = source.indexOf("endStudioEntryBootstrap(world, provider)");
int entryReady = source.indexOf(
"entryResolution.chunk().get(STUDIO_ENTRY_LOAD_TIMEOUT_SECONDS, TimeUnit.SECONDS)", completionCall);
int teleport = source.indexOf(
"WorldRuntimeControlService.get().teleport(player, safeEntry)", entryReady);
"WorldRuntimeControlService.get().teleportInMode(", completionCall);
int finalizeOpen = source.indexOf(
"updateStage(request, \"finalize_open\", 1.00D)", teleport);
int futureComplete = source.indexOf(
"future.complete(new StudioOpenResult(world, safeEntry))", finalizeOpen);
"future.complete(new StudioOpenResult(world, entryLocation))", finalizeOpen);
int methodStart = source.indexOf(
"private void endStudioEntryBootstrap(World world, PlatformChunkGenerator provider)");
int methodEnd = source.indexOf("private void abandonStudioEntryBootstrap", methodStart);
@@ -188,8 +191,8 @@ public class StudioOpenCoordinatorOpenKindTest {
int gateRelease = method.indexOf("bukkitGenerator::endStudioEntryBootstrap");
assertTrue(completionCall >= 0);
assertTrue(entryReady > completionCall);
assertTrue(teleport > entryReady);
assertTrue(teleport > completionCall);
assertFalse(source.contains("preparedEntryChunks"));
assertTrue(finalizeOpen > teleport);
assertTrue(futureComplete > finalizeOpen);
assertTrue(scheduled >= 0);
@@ -208,7 +211,7 @@ public class StudioOpenCoordinatorOpenKindTest {
"src/main/java/art/arcane/iris/core/runtime/StudioOpenCoordinator.java")).replace("\r\n", "\n");
int methodStart = source.indexOf(
"private void abandonStudioEntryBootstrap(World world, Throwable failure)");
int methodEnd = source.indexOf("private void deferFailedOpenCleanup", methodStart);
int methodEnd = source.indexOf("private CompletableFuture<Void> cleanupFailedOpen", methodStart);
String method = source.substring(methodStart, methodEnd);
assertTrue(method.contains("if (J.isPrimaryThread())"));
@@ -247,7 +250,7 @@ public class StudioOpenCoordinatorOpenKindTest {
"src/main/java/art/arcane/iris/core/runtime/StudioOpenCoordinator.java")).replace("\r\n", "\n");
int finalizerCall = source.indexOf("runOpenFinalizer(request.onDone(), world);");
int futureCompletion = source.indexOf(
"future.complete(new StudioOpenResult(world, safeEntry))", finalizerCall);
"future.complete(new StudioOpenResult(world, entryLocation))", finalizerCall);
int methodStart = source.indexOf(
"private void runOpenFinalizer(Consumer<World> finalizer, World world)");
int methodEnd = source.indexOf("private long elapsedMillis", methodStart);
@@ -260,21 +263,4 @@ public class StudioOpenCoordinatorOpenKindTest {
assertTrue(method.contains(
"completion.get(STUDIO_STRUCTURE_ACTIVATION_TIMEOUT_SECONDS, TimeUnit.SECONDS)"));
}
private static StudioOpenCoordinator.StudioOpenRequest request(
StudioOpenCoordinator.StudioOpenKind openKind,
String playerName
) {
return new StudioOpenCoordinator.StudioOpenRequest(
"overworld",
null,
null,
1337L,
"iris-test",
playerName,
openKind,
false,
null,
null);
}
}
@@ -11,10 +11,10 @@ import static org.junit.Assert.assertTrue;
public class StudioOpenCoordinatorSpawnStuckRegressionTest {
@Test
public void waitForSafeEntryRetryLoopIsRemoved() {
public void legacySafeEntryRetryLoopIsRemoved() {
boolean found = Arrays.stream(StudioOpenCoordinator.class.getDeclaredMethods())
.anyMatch(m -> m.getName().equals("waitForSafeEntry"));
assertFalse("waitForSafeEntry retry loop must be removed — it burns up to 120s on ocean columns", found);
assertFalse("waitForSafeEntry retry loop must remain removed", found);
}
@Test
@@ -32,31 +32,23 @@ public class StudioOpenCoordinatorSpawnStuckRegressionTest {
}
@Test
public void entryChunkLoadUsesTheUrgentAsyncRequestWithoutRetentionTickets() throws Exception {
public void entryTeleportDoesNotIssueASecondChunkRequest() throws Exception {
String source = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/runtime/StudioOpenCoordinator.java")).replace("\r\n", "\n");
int asyncLoad = source.indexOf("requested = WorldRuntimeControlService.get().requestChunkAsync(");
int urgentFlag = source.indexOf("true);", asyncLoad);
assertTrue(asyncLoad >= 0);
assertTrue(urgentFlag > asyncLoad);
assertFalse(source.contains("addPluginChunkTicket"));
assertFalse(source.contains("removePluginChunkTicket"));
assertTrue(source.contains("WorldRuntimeControlService.get().teleportInMode("));
assertFalse(source.contains("prepareStudioEntryChunks("));
assertFalse(source.contains("requestChunkAsync("));
}
@Test
public void foliaUsesTheSameNonBlockingAsyncEntryPath() throws Exception {
public void foliaEntryPathNeverReadsTerrainOnARegionThread() throws Exception {
String source = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/runtime/StudioOpenCoordinator.java")).replace("\r\n", "\n");
int loadStart = source.indexOf("private EntryChunkResolution loadEntryChunk(");
int loadEnd = source.indexOf("private void settleEntryUseAfterOperation(", loadStart);
String load = source.substring(loadStart, loadEnd);
assertTrue(load.contains("requestChunkAsync("));
assertTrue(load.contains("J.isOwnedByCurrentRegion(world, chunkX, chunkZ)"));
assertTrue(load.contains("J.runRegion(world, chunkX, chunkZ"));
assertTrue(load.contains("findTopSafeStudioLocation(world, entryAnchor)"));
assertTrue(source.contains("resolveEntryAnchor(world, provider)"));
assertFalse(source.contains("findStudioEntryLocation"));
assertFalse(source.contains("getHighestBlockYAt("));
assertFalse(source.contains("resolveSafeEntry(world, entryAnchor)"));
assertFalse(load.contains("J.isFolia()"));
}
}
@@ -7,7 +7,6 @@ import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.data.Levelled;
import org.bukkit.block.data.Waterlogged;
import org.bukkit.util.BoundingBox;
import org.bukkit.util.VoxelShape;
@@ -78,35 +77,6 @@ public class WorldRuntimeControlServiceSafeEntryTest {
assertEquals(63, result.getBlockY());
}
@Test
public void studioPrefersNearbyDryGroundWithinTheOwnedChunk() {
World world = loadedWorld(0, 0);
Block water = block(Material.WATER, true, true);
Block stone = block(Material.STONE, false, false, FULL_BLOCK);
Block air = block(Material.AIR, false, true);
doReturn(62).when(world).getHighestBlockYAt(anyInt(), anyInt(), eq(HeightMap.MOTION_BLOCKING_NO_LEAVES));
doAnswer(invocation -> {
int x = invocation.getArgument(0);
int y = invocation.getArgument(1);
int z = invocation.getArgument(2);
if (x < 0 || x > 15 || z < 0 || z > 15) {
throw new AssertionError("Safe-entry search crossed its Folia-owned source chunk");
}
if (x == 1 && z == 0) {
return y == 62 ? stone : air;
}
return water;
}).when(world).getBlockAt(anyInt(), anyInt(), anyInt());
Location source = new Location(world, 0.5D, 63D, 0.5D);
Location result = WorldRuntimeControlService.findTopSafeStudioLocation(world, source);
assertNotNull(result);
assertEquals(1, result.getBlockX());
assertEquals(63, result.getBlockY());
assertEquals(0, result.getBlockZ());
}
@Test
public void rejectsFluidHazardousAndCollisionBlockedCandidates() {
World world = loadedWorld(0, 0);
@@ -199,98 +169,6 @@ public class WorldRuntimeControlServiceSafeEntryTest {
assertEquals(-1, lowestReadY.get());
}
@Test
public void studioFallsBackAboveWaterSurfaceAfterExhaustingDryCandidates() {
World world = loadedWorld(0, 0);
Block water = waterBlock(0);
Block air = block(Material.AIR, false, true);
doReturn(62).when(world).getHighestBlockYAt(anyInt(), anyInt(), eq(HeightMap.MOTION_BLOCKING_NO_LEAVES));
doAnswer(invocation -> {
int y = invocation.getArgument(1);
return y <= 62 ? water : air;
}).when(world).getBlockAt(anyInt(), anyInt(), anyInt());
Location source = new Location(world, 0.5D, 63D, 0.5D);
Location result = WorldRuntimeControlService.findTopSafeStudioLocation(world, source);
assertNotNull(result);
assertEquals(0, result.getBlockX());
assertEquals(63, result.getBlockY());
assertEquals(0, result.getBlockZ());
}
@Test
public void studioDoesNotFallBackAboveLavaSurface() {
World world = loadedWorld(0, 0);
Block lava = block(Material.LAVA, true, true);
Block air = block(Material.AIR, false, true);
doReturn(62).when(world).getHighestBlockYAt(anyInt(), anyInt(), eq(HeightMap.MOTION_BLOCKING_NO_LEAVES));
doAnswer(invocation -> {
int y = invocation.getArgument(1);
return y == 62 ? lava : air;
}).when(world).getBlockAt(anyInt(), anyInt(), anyInt());
Location source = new Location(world, 0.5D, 63D, 0.5D);
Location result = WorldRuntimeControlService.findTopSafeStudioLocation(world, source);
assertNull(result);
}
@Test
public void studioRejectsWaterSurfaceWithoutClearHeadroom() {
World world = loadedWorld(0, 0);
Block water = waterBlock(0);
Block air = block(Material.AIR, false, true);
Block stone = block(Material.STONE, false, false, FULL_BLOCK);
doReturn(62).when(world).getHighestBlockYAt(anyInt(), anyInt(), eq(HeightMap.MOTION_BLOCKING_NO_LEAVES));
doAnswer(invocation -> {
int y = invocation.getArgument(1);
if (y <= 62) {
return water;
}
return y == 64 ? stone : air;
}).when(world).getBlockAt(anyInt(), anyInt(), anyInt());
Location source = new Location(world, 0.5D, 63D, 0.5D);
Location result = WorldRuntimeControlService.findTopSafeStudioLocation(world, source);
assertNull(result);
}
@Test
public void studioRejectsFlowingWaterSurface() {
World world = loadedWorld(0, 0);
Block flowingWater = waterBlock(1);
Block air = block(Material.AIR, false, true);
doReturn(62).when(world).getHighestBlockYAt(anyInt(), anyInt(), eq(HeightMap.MOTION_BLOCKING_NO_LEAVES));
doAnswer(invocation -> {
int y = invocation.getArgument(1);
return y <= 62 ? flowingWater : air;
}).when(world).getBlockAt(anyInt(), anyInt(), anyInt());
Location source = new Location(world, 0.5D, 63D, 0.5D);
Location result = WorldRuntimeControlService.findTopSafeStudioLocation(world, source);
assertNull(result);
}
@Test
public void studioRejectsUnsupportedShallowWaterSurface() {
World world = loadedWorld(0, 0);
Block water = waterBlock(0);
Block air = block(Material.AIR, false, true);
doReturn(62).when(world).getHighestBlockYAt(anyInt(), anyInt(), eq(HeightMap.MOTION_BLOCKING_NO_LEAVES));
doAnswer(invocation -> {
int y = invocation.getArgument(1);
return y == 62 ? water : air;
}).when(world).getBlockAt(anyInt(), anyInt(), anyInt());
Location source = new Location(world, 0.5D, 63D, 0.5D);
Location result = WorldRuntimeControlService.findTopSafeStudioLocation(world, source);
assertNull(result);
}
@Test
public void returnsNullWithoutReadingAnUnloadedChunk() {
World world = loadedWorld(0, 0);
@@ -331,11 +209,4 @@ public class WorldRuntimeControlServiceSafeEntryTest {
return block;
}
private static Block waterBlock(int level) {
Block block = block(Material.WATER, true, true);
Levelled blockData = mock(Levelled.class);
doReturn(level).when(blockData).getLevel();
doReturn(blockData).when(block).getBlockData();
return block;
}
}
@@ -0,0 +1,100 @@
package art.arcane.iris.core.runtime;
import art.arcane.iris.util.common.scheduling.J;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.junit.Test;
import org.mockito.InOrder;
import org.mockito.MockedStatic;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.TimeoutException;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
public class WorldRuntimeControlServiceTeleportTest {
@Test
public void failedModeTeleportRestoresThePreviousGameMode() {
Player player = mock(Player.class);
Location destination = mock(Location.class);
CompletableFuture<Boolean> nativeTeleport = new CompletableFuture<>();
when(player.getGameMode()).thenReturn(GameMode.SURVIVAL);
try (MockedStatic<J> scheduling = immediateEntityScheduling()) {
CompletableFuture<Boolean> result = WorldRuntimeControlService.scheduleTeleport(
player,
destination,
GameMode.SPECTATOR,
(target, location) -> nativeTeleport);
nativeTeleport.complete(false);
assertFalse(result.join());
InOrder gameModes = inOrder(player);
gameModes.verify(player).setGameMode(GameMode.SPECTATOR);
gameModes.verify(player).setGameMode(GameMode.SURVIVAL);
}
}
@Test
public void exceptionalModeTeleportRestoresThePreviousGameMode() {
Player player = mock(Player.class);
Location destination = mock(Location.class);
CompletableFuture<Boolean> nativeTeleport = new CompletableFuture<>();
when(player.getGameMode()).thenReturn(GameMode.ADVENTURE);
try (MockedStatic<J> scheduling = immediateEntityScheduling()) {
CompletableFuture<Boolean> result = WorldRuntimeControlService.scheduleTeleport(
player,
destination,
GameMode.SPECTATOR,
(target, location) -> nativeTeleport);
nativeTeleport.completeExceptionally(new IllegalStateException("teleport failed"));
assertThrows(CompletionException.class, result::join);
InOrder gameModes = inOrder(player);
gameModes.verify(player).setGameMode(GameMode.SPECTATOR);
gameModes.verify(player).setGameMode(GameMode.ADVENTURE);
}
}
@Test
public void timedOutModeTeleportCancelsNativeWorkAndRestoresThePreviousGameMode() {
Player player = mock(Player.class);
Location destination = mock(Location.class);
CompletableFuture<Boolean> nativeTeleport = new CompletableFuture<>();
when(player.getGameMode()).thenReturn(GameMode.CREATIVE);
try (MockedStatic<J> scheduling = immediateEntityScheduling()) {
CompletableFuture<Boolean> result = WorldRuntimeControlService.scheduleTeleport(
player,
destination,
GameMode.SPECTATOR,
(target, location) -> nativeTeleport);
assertTrue(result.completeExceptionally(new TimeoutException("timed out")));
assertThrows(CompletionException.class, result::join);
assertTrue(nativeTeleport.isCancelled());
InOrder gameModes = inOrder(player);
gameModes.verify(player).setGameMode(GameMode.SPECTATOR);
gameModes.verify(player).setGameMode(GameMode.CREATIVE);
}
}
private static MockedStatic<J> immediateEntityScheduling() {
MockedStatic<J> scheduling = mockStatic(J.class);
scheduling.when(() -> J.runEntity(any(Player.class), any(Runnable.class))).thenAnswer(invocation -> {
invocation.getArgument(1, Runnable.class).run();
return true;
});
return scheduling;
}
}
@@ -10,13 +10,15 @@ import static org.junit.Assert.assertFalse;
public class StudioSVCWorldPackFeedbackContractTest {
@Test
public void persistentPackCopyUsesLocalizedStyledOperatorFeedback() throws Exception {
public void persistentPackCopyUsesLifecycleProgressWithoutLegacySnapshotFeedback() throws Exception {
String source = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/service/StudioSVC.java"
)).replace("\r\n", "\n");
assertEquals(1, occurrences(source, "STUDIO_S_V_C_PACK_COPY_REQUIRES_ASYNC_THREAD"));
assertEquals(2, occurrences(source, "STUDIO_S_V_C_PACK_INSTALL_FAILED"));
assertFalse(source.contains("STUDIO_S_V_C_INSTALLING_PACKAGE"));
assertFalse(source.contains("Publishing snapshot"));
assertFalse(source.contains("sender.sendMessage(\"Iris refused to copy a pack"));
assertFalse(source.contains("sender.sendMessage(\"Failed to install studio pack"));
}
@@ -26,6 +26,8 @@ import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
public class IrisCreatorTeleportTest {
@Test
@@ -50,7 +52,7 @@ public class IrisCreatorTeleportTest {
Chunk chunk = mock(Chunk.class);
WorldRuntimeControlService runtimeControl = mock(WorldRuntimeControlService.class);
Location anchor = new Location(world, 32.5D, 80D, -15.5D);
Location safeEntry = new Location(world, 32.5D, 94D, -15.5D);
Location safeEntry = new Location(world, 33.5D, 91D, -14.5D);
doReturn(anchor).when(runtimeControl).resolveEntryAnchor(world);
doReturn(CompletableFuture.completedFuture(chunk))
.when(runtimeControl).requestChunkAsync(world, 2, -1, true);
@@ -67,6 +69,7 @@ public class IrisCreatorTeleportTest {
order.verify(runtimeControl).requestChunkAsync(world, 2, -1, true);
order.verify(runtimeControl).resolveSafeEntry(world, anchor);
order.verify(runtimeControl).teleport(player, safeEntry);
verifyNoMoreInteractions(runtimeControl);
}
@Test
@@ -89,7 +92,22 @@ public class IrisCreatorTeleportTest {
}
@Test
public void teleportSenderToCreatedWorld_failsWhenSafeEntryCannotResolve() {
public void teleportSenderToCreatedWorld_failsWhenEntryAnchorCannotResolve() {
Player player = mock(Player.class);
World world = mock(World.class);
WorldRuntimeControlService runtimeControl = mock(WorldRuntimeControlService.class);
doReturn("irisworld").when(world).getName();
doReturn(null).when(runtimeControl).resolveEntryAnchor(world);
CompletableFuture<Boolean> result = IrisCreator.teleportSenderToCreatedWorld(player, world, runtimeControl);
assertThrows(CompletionException.class, result::join);
verify(runtimeControl).resolveEntryAnchor(world);
verifyNoMoreInteractions(runtimeControl);
}
@Test
public void teleportSenderToCreatedWorld_failsWhenNoSafeEntryExists() {
Player player = mock(Player.class);
World world = mock(World.class);
WorldRuntimeControlService runtimeControl = mock(WorldRuntimeControlService.class);
@@ -104,6 +122,11 @@ public class IrisCreatorTeleportTest {
CompletableFuture<Boolean> result = IrisCreator.teleportSenderToCreatedWorld(player, world, runtimeControl);
assertThrows(CompletionException.class, result::join);
InOrder order = inOrder(runtimeControl);
order.verify(runtimeControl).resolveEntryAnchor(world);
order.verify(runtimeControl).requestChunkAsync(world, 0, 0, true);
order.verify(runtimeControl).resolveSafeEntry(world, anchor);
verifyNoMoreInteractions(runtimeControl);
}
@Test
@@ -59,18 +59,38 @@ public class WorldCreationProgressReporterTest {
}
@Test
public void bukkitHudMutationIsMarshalledOffTheAsyncReporterThread() throws Exception {
public void terminalPlayerMessagesHonorStrictPlaceholderContracts() {
String ready = C.stripColor(WorldCreationProgressReporter.terminalPlayerMessage(
false,
1.0D,
"World ready",
"",
1_000L
));
String failed = C.stripColor(WorldCreationProgressReporter.terminalPlayerMessage(
true,
0.98D,
"Entering world",
"",
1_000L
));
assertTrue(ready.contains("100% | World ready"));
assertTrue(failed.contains("FAILED | Entering world"));
}
@Test
public void playerProgressUsesTheEntityScheduledActionBarWithoutABossBar() throws Exception {
String source = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/tools/WorldCreationProgressReporter.java"
)).replace("\r\n", "\n");
assertEquals(1, occurrences(source, "Bukkit.createBossBar("));
assertTrue(source.contains("J.sfut(reporter::initializePlayerHud)"));
assertFalse(source.contains("Bukkit.createBossBar("));
assertFalse(source.contains("WORLD_CREATE_BOSSBAR_"));
assertFalse(source.contains("isProgressBossBar()"));
assertTrue(source.contains("RuntimeProgressMessages.WORLD_CREATE_LIFECYCLE_ACTION"));
assertTrue(source.contains("J.runEntity(sender.player(), guardedRender)"));
assertTrue(source.contains("J.runEntity(sender.player(), render)"));
assertTrue(source.contains("J.runEntity(sender.player(), cleanup, 60, cleanup)"));
assertFalse(source.contains("HudSlotRequest"));
assertFalse(source.contains("hudLanes()"));
}
private static int occurrences(String value, String match) {
@@ -1,18 +1,51 @@
package art.arcane.iris.engine;
import org.junit.Test;
import org.bukkit.World;
import org.bukkit.entity.LivingEntity;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class IrisWorldManagerMarkerTest {
@Test
public void cancelledMantleCleanupLeavesNoDelayedExecutorEntry() {
ScheduledThreadPoolExecutor executor = IrisWorldManager.createCleanupExecutor("test");
try {
ScheduledFuture<?> future = executor.schedule(() -> {
}, 1, TimeUnit.DAYS);
assertEquals(1, executor.getQueue().size());
assertTrue(future.cancel(false));
assertEquals(0, executor.getQueue().size());
} finally {
executor.shutdownNow();
}
}
@Test
public void mantleCleanupDoesNotExecuteDelayedTasksAfterShutdown() throws InterruptedException {
ScheduledThreadPoolExecutor executor = IrisWorldManager.createCleanupExecutor("test");
AtomicBoolean executed = new AtomicBoolean();
executor.schedule(() -> executed.set(true), 1, TimeUnit.DAYS);
executor.shutdown();
assertTrue(executor.awaitTermination(1, TimeUnit.SECONDS));
assertFalse(executed.get());
assertEquals(0, executor.getQueue().size());
}
@Test
public void worldBlockHeightIsTranslatedToMantleHeight() {
assertEquals(64, WorldBlockDropRouter.toMantleY(0, -64));
@@ -28,28 +61,13 @@ public class IrisWorldManagerMarkerTest {
}
@Test
public void completedEntityTasksAreAccepted() {
assertTrue(WorldEntitySpawner.awaitEntityTasks(new CountDownLatch(0), 0, TimeUnit.MILLISECONDS));
}
public void entitySaturationUsesTheCompleteLivingEntitySnapshot() {
World world = mock(World.class);
LivingEntity first = mock(LivingEntity.class);
LivingEntity second = mock(LivingEntity.class);
when(world.getLivingEntities()).thenReturn(List.of(first, second));
@Test
public void incompleteEntityTasksAreRejected() {
assertFalse(WorldEntitySpawner.awaitEntityTasks(new CountDownLatch(1), 0, TimeUnit.MILLISECONDS));
}
@Test
public void interruptedEntityWaitPreservesInterruptStatus() throws Exception {
AtomicBoolean preserved = new AtomicBoolean();
Thread thread = new Thread(() -> {
Thread.currentThread().interrupt();
boolean completed = WorldEntitySpawner.awaitEntityTasks(new CountDownLatch(1), 1, TimeUnit.SECONDS);
preserved.set(!completed && Thread.currentThread().isInterrupted());
});
thread.start();
thread.join();
assertTrue(preserved.get());
assertEquals(2, WorldEntitySpawner.livingEntityCount(world));
}
@Test
@@ -2,16 +2,21 @@ package art.arcane.iris.engine.mantle.components;
import art.arcane.iris.engine.river.cave.CavePosition;
import art.arcane.iris.engine.river.cave.CaveVoxel;
import art.arcane.iris.engine.river.cave.RiverCaveAction;
import art.arcane.iris.engine.river.cave.RiverCaveHydrology;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.mantle.runtime.TectonicPlate;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.matter.MatterSlice;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.when;
public class MantleRiverCaveVoxelViewTest {
@@ -34,4 +39,33 @@ public class MantleRiverCaveVoxelViewTest {
assertEquals(CaveVoxel.SOLID, view.voxelAt(terrain));
assertFalse(view.isOpenToSurface(terrain));
}
@Test
@SuppressWarnings("unchecked")
public void publishedRiverActionsDoNotReplaceTheUnderlyingPlanningBaseline() {
Mantle<Matter> mantle = mock(Mantle.class);
TectonicPlate<Matter> plate = mock(TectonicPlate.class);
MantleChunk<Matter> chunk = mock(MantleChunk.class);
Matter matter = mock(Matter.class);
MatterSlice<RiverCaveHydrology> hydrologySlice = mock(MatterSlice.class);
KMap<Long, TectonicPlate<Matter>> regions = new KMap<>();
regions.put(Mantle.key(0, 0), plate);
CavePosition position = new CavePosition(0, 20, 0);
when(mantle.getLoadedRegions()).thenReturn(regions);
when(plate.get(0, 0)).thenReturn(chunk);
when(chunk.exists(1)).thenReturn(true);
when(chunk.get(1)).thenReturn(matter);
when(matter.hasSlice(RiverCaveHydrology.class)).thenReturn(true);
doReturn(hydrologySlice).when(matter).getSlice(RiverCaveHydrology.class);
when(hydrologySlice.get(0, 4, 0)).thenReturn(RiverCaveHydrology.of(RiverCaveAction.WET_SOURCE));
MantleRiverCaveVoxelView view = new MantleRiverCaveVoxelView(
mantle,
128,
(x, z) -> 60,
(x, z) -> null
);
assertEquals(CaveVoxel.SOLID, view.voxelAt(position));
assertEquals(RiverCaveAction.WET_SOURCE, view.riverActionAt(position));
}
}
@@ -99,6 +99,7 @@ public class MantleRiverHydrologyComponentTest {
0,
0,
1,
2,
(minimumX, minimumZ, maximumX, maximumZ) -> riverSample(
RiverRouteState.WET,
RiverSection.CHANNEL
@@ -116,9 +117,10 @@ public class MantleRiverHydrologyComponentTest {
}
@Test
public void buriedChannelCapsBeforeAnUnrelatedCave() {
public void buriedChannelCutsIntoCaveWhileGuardingItsWaterline() {
TestVoxelView view = new TestVoxelView();
view.set(new CavePosition(1, 12, 0), CaveVoxel.CAVE_AIR);
view.set(new CavePosition(1, 14, 0), CaveVoxel.CAVE_AIR);
IrisRiverTunnelSample tunnel = new IrisRiverTunnelSample(
riverSample(RiverRouteState.WET, RiverSection.CHANNEL),
10,
@@ -131,6 +133,72 @@ public class MantleRiverHydrologyComponentTest {
0,
0,
1,
2,
(minimumX, minimumZ, maximumX, maximumZ) -> riverSample(
RiverRouteState.WET,
RiverSection.CHANNEL
),
(x, z) -> x == 0 && z == 0 ? tunnel : null,
(x, z) -> IrisRiverSurfaceSample.none(70D, 63D)
);
assertEquals(RiverCaveAction.WET_SOURCE, plan.actions().get(new CavePosition(0, 12, 0)));
assertEquals(RiverCaveAction.SEAL_GUARD, plan.actions().get(new CavePosition(1, 12, 0)));
assertFalse(plan.actions().containsKey(new CavePosition(1, 14, 0)));
assertEquals(
new CaveVoxelPrecondition(CaveVoxel.CAVE_AIR, false),
plan.preconditions().get(new CavePosition(1, 14, 0))
);
assertTrue(MantleRiverHydrologyComponent.preconditionsHold(view, plan.preconditions()));
}
@Test
public void buriedChannelRejectsSurfaceOpenCaveAir() {
TestVoxelView view = new TestVoxelView();
CavePosition contact = new CavePosition(1, 12, 0);
view.set(contact, CaveVoxel.CAVE_AIR);
view.open(contact);
IrisRiverTunnelSample tunnel = new IrisRiverTunnelSample(
riverSample(RiverRouteState.WET, RiverSection.CHANNEL),
10,
12,
14
);
MantleRiverHydrologyComponent.TunnelPlan plan = MantleRiverHydrologyComponent.planTunnels(
view,
0,
0,
1,
2,
(minimumX, minimumZ, maximumX, maximumZ) -> riverSample(
RiverRouteState.WET,
RiverSection.CHANNEL
),
(x, z) -> x == 0 && z == 0 ? tunnel : null,
(x, z) -> IrisRiverSurfaceSample.none(70D, 63D)
);
assertTrue(plan.actions().isEmpty());
}
@Test
public void buriedChannelStillRejectsAnUncontainedFluidCave() {
TestVoxelView view = new TestVoxelView();
view.set(new CavePosition(1, 12, 0), CaveVoxel.COMPATIBLE_FLUID);
IrisRiverTunnelSample tunnel = new IrisRiverTunnelSample(
riverSample(RiverRouteState.WET, RiverSection.CHANNEL),
10,
12,
14
);
MantleRiverHydrologyComponent.TunnelPlan plan = MantleRiverHydrologyComponent.planTunnels(
view,
0,
0,
1,
2,
(minimumX, minimumZ, maximumX, maximumZ) -> riverSample(
RiverRouteState.WET,
RiverSection.CHANNEL
@@ -148,6 +216,8 @@ public class MantleRiverHydrologyComponentTest {
TestVoxelView view = new TestVoxelView();
view.set(new CavePosition(1, 11, 0), CaveVoxel.COMPATIBLE_FLUID);
view.set(new CavePosition(1, 12, 0), CaveVoxel.COMPATIBLE_FLUID);
view.set(new CavePosition(1, 13, 0), CaveVoxel.CAVE_AIR);
view.set(new CavePosition(1, 14, 0), CaveVoxel.CAVE_AIR);
IrisRiverTunnelSample tunnel = new IrisRiverTunnelSample(
riverSample(RiverRouteState.WET, RiverSection.CHANNEL),
10,
@@ -168,6 +238,7 @@ public class MantleRiverHydrologyComponentTest {
0,
0,
1,
2,
(minimumX, minimumZ, maximumX, maximumZ) -> riverSample(
RiverRouteState.WET,
RiverSection.CHANNEL
@@ -177,6 +248,7 @@ public class MantleRiverHydrologyComponentTest {
);
assertEquals(RiverCaveAction.WET_SOURCE, plan.actions().get(new CavePosition(0, 12, 0)));
assertEquals(RiverCaveAction.DRY_AIR, plan.actions().get(new CavePosition(0, 14, 0)));
assertFalse(plan.preconditions().containsKey(new CavePosition(1, 12, 0)));
}
@@ -199,6 +271,7 @@ public class MantleRiverHydrologyComponentTest {
0,
0,
1,
2,
(minimumX, minimumZ, maximumX, maximumZ) -> riverSample(
RiverRouteState.WET,
RiverSection.CHANNEL
@@ -343,6 +416,29 @@ public class MantleRiverHydrologyComponentTest {
assertFalse(plan.actions().containsKey(connectedPit));
}
@Test
public void generatedTargetKeepsItsGrottoShellBelowTheRiverBed() {
IrisRiverCaves caves = caves()
.setDryHeadroom(4)
.setGrottoVerticalRadius(6)
.setMaxBoreDepth(64);
CavePosition entry = new CavePosition(0, 54, 0);
int waterHeadY = 48;
CavePosition target = MantleRiverHydrologyComponent.findGeneratedTarget(
new TestVoxelView(),
caves,
entry,
waterHeadY,
0,
0
);
assertEquals(new CavePosition(0, 46, 0), target);
assertTrue(target.y() + caves.getGrottoVerticalRadius() < entry.y());
assertTrue(target.y() + caves.getGrottoVerticalRadius() >= waterHeadY + caves.getDryHeadroom());
}
@Test
public void fluidPolicyMappingPreservesReplaceSemantics() {
assertEquals(
@@ -436,6 +532,7 @@ public class MantleRiverHydrologyComponentTest {
3,
-4,
6,
2,
(minimumX, minimumZ, maximumX, maximumZ) -> {
footprintSamples.incrementAndGet();
assertEquals(42D, minimumX, 0D);
@@ -5,6 +5,8 @@ import art.arcane.iris.core.loader.ResourceLoader;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDimensionCarvingResolver;
import art.arcane.iris.engine.river.cave.RiverCaveAction;
import art.arcane.iris.engine.river.cave.RiverCaveHydrology;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.project.hunk.Hunk;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
@@ -107,10 +109,52 @@ public class IrisCarveModifierBoundarySupportTest {
assertTrue(IrisCarveModifier.hasStableCaveFloorSupport(output, 0, 6, 0));
}
@Test
public void riverGuardOnlyAcceptsStableSolidBoundaryLayers() {
RiverCaveHydrology guard = RiverCaveHydrology.of(RiverCaveAction.SEAL_GUARD);
PlatformBlockState stone = state("minecraft:stone", true);
PlatformBlockState sand = state("minecraft:sand", true);
PlatformBlockState water = state("minecraft:water", false, true);
assertTrue(IrisCarveModifier.canReplaceRiverGuard(guard, stone, false));
assertTrue(IrisCarveModifier.canReplaceRiverGuard(guard, stone, true));
assertTrue(IrisCarveModifier.canReplaceRiverGuard(guard, sand, false));
assertFalse(IrisCarveModifier.canReplaceRiverGuard(guard, sand, true));
assertFalse(IrisCarveModifier.canReplaceRiverGuard(guard, water, false));
}
@Test
public void riverBiomeInheritanceIsColumnCoherentAndHonorsLimits() {
long seed = 7845123L;
boolean cell = IrisCarveModifier.selectsParentRiverBiome(seed, 8, 12, 0.5D);
for (int x = 8; x < 12; x++) {
for (int z = 12; z < 16; z++) {
assertTrue(cell == IrisCarveModifier.selectsParentRiverBiome(seed, x, z, 0.5D));
}
}
assertFalse(IrisCarveModifier.selectsParentRiverBiome(seed, 8, 12, 0D));
assertTrue(IrisCarveModifier.selectsParentRiverBiome(seed, 8, 12, 1D));
boolean inherited = false;
boolean overridden = false;
for (int x = -128; x <= 128; x += 4) {
boolean selected = IrisCarveModifier.selectsParentRiverBiome(seed, x, 0, 0.5D);
inherited |= selected;
overridden |= !selected;
}
assertTrue(inherited);
assertTrue(overridden);
}
private PlatformBlockState state(String key, boolean solid) {
return state(key, solid, false);
}
private PlatformBlockState state(String key, boolean solid, boolean fluid) {
PlatformBlockState state = mock(PlatformBlockState.class);
doReturn(key).when(state).key();
doReturn(solid).when(state).isSolid();
doReturn(fluid).when(state).isFluid();
return state;
}
}
@@ -70,7 +70,7 @@ public class IrisCarveModifierInferenceIsolationTest {
Map<String, IrisBiome> customBiomes = new HashMap<>();
customBiomes.put("shared", biome);
CarveWallBuffer walls = new CarveWallBuffer(1);
walls.put(0, 1, 0, new MatterCavern(true, "shared", (byte) 0));
walls.put(0, 1, 0, new MatterCavern(true, "shared", (byte) 0), false);
Method paintBoundaryZone = IrisCarveModifier.class.getDeclaredMethod(
"paintBoundaryZone",
Hunk.class,
@@ -22,19 +22,21 @@ public class IrisCarveScratchTest {
int y = 20 + index;
int z = (index * 3) & 15;
MatterCavern cavern = new MatterCavern(true, "cave-" + index, (byte) 0);
buffer.put(x, y, z, cavern);
buffer.put(x, y, z, cavern, (index & 1) == 0);
expected.put(key(x, y, z), cavern);
}
MatterCavern replacement = new MatterCavern(true, "replacement", (byte) 0);
buffer.put(5, 25, 15, replacement);
buffer.put(5, 25, 15, replacement, true);
expected.put(key(5, 25, 15), replacement);
assertSame(replacement, buffer.get(5, 25, 15));
assertTrue(buffer.isRiverBoundary(5, 25, 15));
assertNull(buffer.get(5, 26, 15));
assertFalse(buffer.isRiverBoundary(5, 26, 15));
Map<String, MatterCavern> actual = new HashMap<>();
buffer.forEach((x, y, z, cavern) -> actual.put(key(x, y, z), cavern));
buffer.forEach((x, y, z, cavern, riverBoundary) -> actual.put(key(x, y, z), cavern));
assertEquals(expected.keySet(), actual.keySet());
for (Map.Entry<String, MatterCavern> entry : expected.entrySet()) {
assertSame(entry.getValue(), actual.get(entry.getKey()));
@@ -47,7 +49,7 @@ public class IrisCarveScratchTest {
MatterCavern cavern = new MatterCavern(true, "cave", (byte) 0);
scratch.columnMasks[0].add(12);
scratch.boundaryMasks[0].add(13);
scratch.walls.put(1, 12, 2, cavern);
scratch.walls.put(1, 12, 2, cavern, true);
scratch.customBiomeCache.put("cave", null);
scratch.customCaveBiomePresent = true;
@@ -58,7 +60,7 @@ public class IrisCarveScratchTest {
assertTrue(scratch.customBiomeCache.isEmpty());
assertFalse(scratch.customCaveBiomePresent);
int[] wallCount = new int[1];
scratch.walls.forEach((x, y, z, value) -> wallCount[0]++);
scratch.walls.forEach((x, y, z, value, riverBoundary) -> wallCount[0]++);
assertEquals(0, wallCount[0]);
}
@@ -2,6 +2,7 @@ package art.arcane.iris.engine.object;
import org.junit.Test;
import static art.arcane.iris.engine.object.IrisDimensionTypeOptions.TriState.TRUE;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@@ -79,6 +80,60 @@ public class IrisDimensionRuntimeContractTest {
}
}
@Test
public void hotloadRejectsEnvironmentChanges() {
IrisDimension active = hotloadDimension();
IrisDimension replacement = hotloadDimension();
replacement.setEnvironment(IrisEnvironment.NETHER);
assertHotloadRejected(active, replacement, "active environment is NORMAL");
}
@Test
public void hotloadRejectsCustomEnvironmentEvenWhenItsBaseTypeMatchesNormal() {
IrisDimension active = hotloadDimension();
IrisDimension replacement = hotloadDimension();
replacement.setEnvironment(IrisEnvironment.CUSTOM);
assertHotloadRejected(active, replacement, "replacement environment is CUSTOM");
}
@Test
public void hotloadRejectsEffectiveDimensionOptionChanges() {
IrisDimension active = hotloadDimension();
IrisDimension replacement = hotloadDimension();
replacement.setDimensionOptions(new IrisDimensionTypeOptions().coordinateScale(2D));
assertHotloadRejected(active, replacement, "dimensionOptions");
}
@Test
public void hotloadRejectsFullbrightChangesThatAlterTheGeneratedType() {
IrisDimension active = hotloadDimension();
IrisDimension replacement = hotloadDimension();
replacement.setFullbright(true);
assertHotloadRejected(active, replacement, "fullbright");
}
@Test
public void hotloadAllowsExplicitOptionsEquivalentToTheBaseTemplate() {
IrisDimension active = hotloadDimension();
IrisDimension replacement = hotloadDimension();
replacement.setDimensionOptions(new IrisDimensionTypeOptions().skylight(TRUE));
IrisDimensionRuntimeContract.requireHotloadCompatible("Studio world", active, replacement, "iris");
}
@Test
public void hotloadAllowsTerrainOnlyDimensionChanges() {
IrisDimension active = hotloadDimension();
IrisDimension replacement = hotloadDimension();
replacement.setFluidHeight(80);
IrisDimensionRuntimeContract.requireHotloadCompatible("Studio world", active, replacement, "iris");
}
private IrisDimensionRuntimeContract expectedContract() {
IrisDimension dimension = new IrisDimension();
dimension.setLoadKey("overworld");
@@ -86,4 +141,22 @@ public class IrisDimensionRuntimeContractTest {
dimension.setLogicalHeight(512);
return IrisDimensionRuntimeContract.expected(dimension, "iris");
}
private IrisDimension hotloadDimension() {
IrisDimension dimension = new IrisDimension();
dimension.setLoadKey("overworld");
dimension.setEnvironment(IrisEnvironment.NORMAL);
dimension.setDimensionHeight(new IrisRange(-64, 320));
dimension.setLogicalHeight(384);
return dimension;
}
private void assertHotloadRejected(IrisDimension active, IrisDimension replacement, String messageFragment) {
try {
IrisDimensionRuntimeContract.requireHotloadCompatible("Studio world", active, replacement, "iris");
fail("A hotload cannot replace the generated dimension type contract");
} catch (IrisDimensionContractException e) {
assertTrue(e.getMessage().contains(messageFragment));
}
}
}
@@ -6,6 +6,7 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
@@ -20,6 +21,8 @@ import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class RiverNetworkTest {
private static final double TWO_PI_FOR_TESTS = StrictMath.PI * 2D;
@Test
public void mapsNegativeWorldCoordinatesWithFloorDivision() {
RiverNetwork network = new RiverNetwork(options(1L).build());
@@ -32,6 +35,28 @@ public class RiverNetworkTest {
assertEquals(0, network.tileXForBlock(0));
}
@Test
public void expandedSamplingAddsOnlyTheRequestedLateralRadius() {
RiverNode from = node(0L, 0L, 0D, 0D);
RiverNode to = node(1L, 0L, 100D, 0D);
RiverTile tile = new RiverTile(
0,
0,
0,
-64,
128,
64,
List.of(reach(from, to, 4D, 1D))
);
assertFalse(tile.sample(50D, 5D).present());
RiverSample expanded = tile.sampleExpanded(50D, 5D, 2D);
assertTrue(expanded.present());
assertEquals(RiverSection.BANK, expanded.section());
assertEquals(5D, expanded.distance(), 0D);
assertThrows(IllegalArgumentException.class, () -> tile.sampleExpanded(50D, 5D, -1D));
}
@Test
public void graphHasReciprocalCardinalsAndOneStableDiagonalPerSquare() {
RiverNetwork network = new RiverNetwork(options(2L).build());
@@ -138,6 +163,30 @@ public class RiverNetworkTest {
assertEquals(digest(tile), digest(network.buildTile(0, 0, terrain)));
}
@Test
public void recursiveBranchesApplyTheChildSoftCapAtEveryNode() {
RiverNetwork network = new RiverNetwork(options(720L)
.requireOcean(false)
.routingBasinCells(16)
.branchSoftCap(4)
.branchChildShrinkFactor(0D)
.build());
RiverTerrainSampler terrain = flatTerrain(false);
HashMap<RiverNodeId, Integer> incoming = new HashMap<>();
for (long cellX = -24L; cellX <= 24L; cellX++) {
for (long cellZ = -24L; cellZ <= 24L; cellZ++) {
RiverNode downstream = network.downstream(new RiverNodeId(cellX, cellZ), terrain);
if (downstream != null) {
incoming.merge(downstream.id(), 1, Integer::sum);
}
}
}
assertFalse(incoming.isEmpty());
assertTrue(incoming.values().stream().allMatch(children -> children <= 4));
assertTrue(incoming.values().stream().anyMatch(children -> children >= 3));
}
@Test
public void candidateRankingAndTracingDoNotMaterializeMeanderGeometry() {
RiverNetwork network = new RiverNetwork(options(75L)
@@ -167,8 +216,8 @@ public class RiverNetworkTest {
assertFalse(tile.reaches().isEmpty());
assertTrue(meanderSamples[0] > 0);
assertEquals(0, meanderSamples[0] % 33);
assertTrue(meanderSamples[0] / 33 <= 256);
assertEquals(0, meanderSamples[0] % 12);
assertTrue(meanderSamples[0] / 12 <= 256);
}
@Test
@@ -330,27 +379,33 @@ public class RiverNetworkTest {
assertFalse(wet.edges().isEmpty());
RiverNetwork dryNetwork = new RiverNetwork(options(4L).maxRouteReaches(6).build());
RiverRoute dry = dryNetwork.trace(source, slopedTerrain(false));
RiverTerrainSampler physicalTerminal = new TestTerrain(false) {
@Override
public boolean allowsReach(RiverRoutingContext context) {
return context.from().id().equals(source);
}
};
RiverRoute dry = dryNetwork.trace(source, physicalTerminal);
assertEquals(RiverRouteState.DRY, dry.state());
assertFalse(dry.edges().isEmpty());
RiverNetwork suppressedNetwork = new RiverNetwork(
options(4L).maxRouteReaches(6).dryChannelChance(0.0).build()
);
RiverRoute suppressed = suppressedNetwork.trace(source, slopedTerrain(false));
RiverRoute suppressed = suppressedNetwork.trace(source, physicalTerminal);
assertEquals(RiverRouteState.SUPPRESSED, suppressed.state());
assertTrue(suppressed.edges().isEmpty());
}
@Test
public void oceanOptionalRoutesRemainWetAndExposeTerminalState() {
public void oceanOptionalRoutesDoNotTurnTheTraceHorizonIntoATerminal() {
RiverNetwork network = new RiverNetwork(
options(41L).maxRouteReaches(3).requireOcean(false).build()
);
RiverRoute route = network.trace(new RiverNodeId(0L, 0L), slopedTerrain(false));
assertEquals(RiverRouteState.WET, route.state());
assertFalse(route.oceanConnected());
assertTrue(route.terminal());
assertFalse(route.terminal());
assertFalse(route.edges().isEmpty());
}
@@ -359,12 +414,22 @@ public class RiverNetworkTest {
RiverNodeId source = new RiverNodeId(0L, 0L);
RiverNetwork requiredOcean = new RiverNetwork(options(42L).maxRouteReaches(3).requireOcean(true).build());
RiverTerrainSampler wetTerminal = new TestTerrain(false) {
@Override
public boolean allowsReach(RiverRoutingContext context) {
return context.from().id().equals(source);
}
@Override
public RiverTerminalPolicy terminalPolicy(int blockX, int blockZ) {
return RiverTerminalPolicy.WET;
}
};
RiverTerrainSampler suppressedTerminal = new TestTerrain(false) {
@Override
public boolean allowsReach(RiverRoutingContext context) {
return context.from().id().equals(source);
}
@Override
public RiverTerminalPolicy terminalPolicy(int blockX, int blockZ) {
return RiverTerminalPolicy.SUPPRESS;
@@ -376,6 +441,11 @@ public class RiverNetworkTest {
RiverNetwork optionalOcean = new RiverNetwork(options(43L).maxRouteReaches(3).requireOcean(false).build());
RiverTerrainSampler dryTerminal = new TestTerrain(false) {
@Override
public boolean allowsReach(RiverRoutingContext context) {
return context.from().id().equals(source);
}
@Override
public RiverTerminalPolicy terminalPolicy(int blockX, int blockZ) {
return RiverTerminalPolicy.DRY;
@@ -546,10 +616,10 @@ public class RiverNetworkTest {
);
assertEquals(List.of(
-6381557853649488119L,
8976828799821705624L,
6774369135184562919L,
-6208289191927305511L
3050685048971830506L,
-3861364772874819248L,
-8770785170128501517L,
6698607697170340377L
), actual);
}
@@ -589,6 +659,7 @@ public class RiverNetworkTest {
.channelWidth(512D)
.maxChannelWidth(512D)
.bankWidth(0D)
.branchChildShrinkFactor(1D)
.meanderStrength(0D)
.meanderSubdivisions(1)
.build());
@@ -716,6 +787,7 @@ public class RiverNetworkTest {
1,
1,
10.0,
RiverWidthProfile.constant(10.0),
5.0,
3.0,
false,
@@ -795,6 +867,7 @@ public class RiverNetworkTest {
.maxChannelWidth(10D)
.maxBankWidth(3D)
.maxDepth(6D)
.meanderStrength(0D)
.build());
RiverTerrainSampler styled = new TestTerrain(false) {
@Override
@@ -832,6 +905,78 @@ public class RiverNetworkTest {
}
}
@Test
public void channelWidthVariesInsideOneReachAndRemainsSpatiallyQueryable() {
RiverNode from = node(0L, 0L, 0D, 0D);
RiverNode to = node(1L, 0L, 100D, 0D);
RiverWidthProfile profile = new RiverWidthProfile(
new double[]{0D, 0.5D, 1D},
new double[]{1D, 7D, 1D}
);
RiverReach reach = new RiverReach(
RiverEdgeId.of(from.id(), to.id()),
from,
to,
RiverRouteState.WET,
1,
1,
7D,
profile,
0D,
3D,
false,
false,
new RiverPolyline(new double[]{0D, 100D}, new double[]{0D, 0D})
);
RiverTile tile = new RiverTile(0, 0, 0, -16, 128, 32, List.of(reach));
RiverSample narrow = tile.sample(10D, 2D);
RiverSample wide = tile.sample(50D, 2D);
assertFalse(narrow.present());
assertTrue(wide.present());
assertEquals(7D, wide.width(), 0D);
assertEquals(RiverSection.CHANNEL, wide.section());
}
@Test
public void foldedReachSamplingFindsFartherCoveringWidthEnvelope() {
RiverNode from = node(0L, 0L, 0D, 0D);
RiverNode to = node(1L, 0L, 0D, 10D);
RiverWidthProfile profile = new RiverWidthProfile(
new double[]{0D, 0.48D, 0.53D, 1D},
new double[]{1D, 1D, 18D, 18D}
);
RiverReach reach = new RiverReach(
RiverEdgeId.of(from.id(), to.id()),
from,
to,
RiverRouteState.WET,
1,
1,
18D,
profile,
0D,
3D,
false,
false,
new RiverPolyline(
new double[]{0D, 100D, 100D, 0D},
new double[]{0D, 0D, 10D, 10D}
)
);
RiverTile tile = new RiverTile(0, 0, 0, -16, 128, 32, List.of(reach));
RiverSample point = tile.sample(50D, 2D);
RiverSample footprint = tile.sampleFootprint(49D, 1.5D, 51D, 2.5D);
assertTrue(point.present());
assertTrue(point.alongReach() > 0.53D);
assertEquals(18D, point.width(), 0.000001D);
assertTrue(footprint.present());
assertTrue(footprint.alongReach() > 0.53D);
}
@Test
public void reachFeasibilityReceivesTheFinalPinnedMeanderPolyline() {
RiverNetwork network = new RiverNetwork(options(62L).meanderStrength(20D).build());
@@ -865,6 +1010,182 @@ public class RiverNetworkTest {
assertEquals(null, downstream);
}
@Test
public void irisNoiseChangesTheSelectedPersonalityWithoutBreakingItsEnvelope() {
double meanderStrength = 20D;
RiverNetwork network = new RiverNetwork(options(63L)
.siteJitter(0D)
.requireOcean(false)
.meanderStrength(meanderStrength)
.meanderSubdivisions(32)
.build());
RiverTerrainSampler quietTerrain = new TestTerrain(false) {
@Override
public double meanderNoise(RiverMeanderContext context) {
return 0D;
}
};
RiverTerrainSampler noisyTerrain = new TestTerrain(false) {
@Override
public double meanderNoise(RiverMeanderContext context) {
return StrictMath.sin(context.normalizedPosition() * StrictMath.PI * 7D);
}
};
RiverReach quietReach = network.buildTile(0, 0, quietTerrain).reaches().getFirst();
RiverReach noisyReach = network.buildTile(0, 0, noisyTerrain).reaches().getFirst();
assertEquals(quietReach.id(), noisyReach.id());
RiverPolyline quietPolyline = quietReach.polyline();
RiverPolyline noisyPolyline = noisyReach.polyline();
double deltaX = noisyReach.to().x() - noisyReach.from().x();
double deltaZ = noisyReach.to().z() - noisyReach.from().z();
double chordLength = StrictMath.hypot(deltaX, deltaZ);
boolean changed = false;
for (int point = 1; point < noisyPolyline.size() - 1; point++) {
double pointX = noisyPolyline.x(point) - noisyReach.from().x();
double pointZ = noisyPolyline.z(point) - noisyReach.from().z();
double signedOffset = (pointX * deltaZ - pointZ * deltaX) / chordLength;
if (Double.doubleToLongBits(noisyPolyline.x(point)) != Double.doubleToLongBits(quietPolyline.x(point))
|| Double.doubleToLongBits(noisyPolyline.z(point))
!= Double.doubleToLongBits(quietPolyline.z(point))) {
changed = true;
}
assertTrue(StrictMath.abs(signedOffset) <= meanderStrength + 0.0000001D);
}
assertTrue(changed);
assertEquals(noisyReach.from().x(), noisyPolyline.x(0), 0D);
assertEquals(noisyReach.from().z(), noisyPolyline.z(0), 0D);
int last = noisyPolyline.size() - 1;
assertEquals(noisyReach.to().x(), noisyPolyline.x(last), 0D);
assertEquals(noisyReach.to().z(), noisyPolyline.z(last), 0D);
}
@Test
public void reachesSpanBroadAsymmetricCompoundAndRestlessShapeFamilies() {
double meanderStrength = 20D;
RiverNetwork network = new RiverNetwork(options(64L)
.siteJitter(0D)
.requireOcean(false)
.meanderStrength(meanderStrength)
.meanderSubdivisions(48)
.build());
RiverTerrainSampler terrain = new TestTerrain(false) {
@Override
public double meanderNoise(RiverMeanderContext context) {
return 0D;
}
};
HashMap<RiverEdgeId, RiverReach> uniqueReaches = new HashMap<>();
for (int tileX = -2; tileX <= 2; tileX++) {
for (int tileZ = -2; tileZ <= 2; tileZ++) {
for (RiverReach reach : network.buildTile(tileX, tileZ, terrain).reaches()) {
uniqueReaches.putIfAbsent(reach.id(), reach);
}
}
}
ArrayList<ReachShape> shapes = new ArrayList<>(uniqueReaches.size());
for (RiverReach reach : uniqueReaches.values()) {
shapes.add(shapeOf(reach, meanderStrength));
}
double minimumAmplitude = shapes.stream()
.mapToDouble(ReachShape::amplitudeRatio)
.min()
.orElseThrow();
double maximumAmplitude = shapes.stream()
.mapToDouble(ReachShape::amplitudeRatio)
.max()
.orElseThrow();
HashSet<Integer> dominantBands = new HashSet<>();
HashSet<String> signatures = new HashSet<>();
for (ReachShape shape : shapes) {
dominantBands.add(shape.dominantBand());
signatures.add(
StrictMath.min(6, shape.crossings()) + ":"
+ (int) StrictMath.floor(shape.centroid() * 5D) + ":"
+ (int) StrictMath.floor(shape.amplitudeRatio() * 5D) + ":"
+ shape.dominantBand()
);
}
String diagnostics = shapes.toString();
assertTrue(diagnostics, shapes.size() >= 32);
assertTrue(diagnostics, shapes.stream().anyMatch(shape -> shape.crossings() <= 1));
assertTrue(diagnostics, shapes.stream().anyMatch(shape -> shape.crossings() >= 2 && shape.crossings() <= 4));
assertTrue(diagnostics, shapes.stream().anyMatch(shape -> shape.crossings() >= 5));
assertTrue(diagnostics, shapes.stream().anyMatch(shape -> shape.centroid() < 0.43D));
assertTrue(diagnostics, shapes.stream().anyMatch(shape -> shape.centroid() > 0.57D));
assertTrue(diagnostics, maximumAmplitude - minimumAmplitude >= 0.25D);
assertTrue(diagnostics, dominantBands.size() >= 3);
assertTrue(diagnostics, signatures.size() >= 8);
}
@Test
public void drainageDomainWarpChangesRoutesDeterministically() {
RiverTerrainSampler terrain = flatTerrain(false);
RiverNetwork straight = new RiverNetwork(options(65L)
.requireOcean(false)
.routingDeviationStrengthCells(0D)
.build());
RiverNetwork deviated = new RiverNetwork(options(65L)
.requireOcean(false)
.routingDeviationScaleCells(8)
.routingDeviationStrengthCells(4D)
.build());
boolean changed = false;
for (long cellX = -8L; cellX <= 8L; cellX++) {
for (long cellZ = -8L; cellZ <= 8L; cellZ++) {
RiverNode straightDownstream = straight.downstream(new RiverNodeId(cellX, cellZ), terrain);
RiverNode deviatedDownstream = deviated.downstream(new RiverNodeId(cellX, cellZ), terrain);
if (!Objects.equals(
straightDownstream == null ? null : straightDownstream.id(),
deviatedDownstream == null ? null : deviatedDownstream.id()
)) {
changed = true;
}
RiverNode repeated = deviated.downstream(new RiverNodeId(cellX, cellZ), terrain);
assertEquals(
deviatedDownstream == null ? null : deviatedDownstream.id(),
repeated == null ? null : repeated.id()
);
}
}
assertTrue(changed);
}
@Test
public void confluenceAttractionChangesDownstreamChoicesDeterministically() {
RiverTerrainSampler terrain = flatTerrain(false);
RiverNetwork dispersed = new RiverNetwork(options(67L)
.requireOcean(false)
.confluenceWeight(0D)
.build());
RiverNetwork branching = new RiverNetwork(options(67L)
.requireOcean(false)
.confluenceWeight(512D)
.build());
boolean changed = false;
for (long cellX = -8L; cellX <= 8L; cellX++) {
for (long cellZ = -8L; cellZ <= 8L; cellZ++) {
RiverNodeId id = new RiverNodeId(cellX, cellZ);
RiverNode dispersedDownstream = dispersed.downstream(id, terrain);
RiverNode branchingDownstream = branching.downstream(id, terrain);
if (!Objects.equals(
dispersedDownstream == null ? null : dispersedDownstream.id(),
branchingDownstream == null ? null : branchingDownstream.id()
)) {
changed = true;
}
RiverNode repeated = branching.downstream(id, terrain);
assertEquals(
branchingDownstream == null ? null : branchingDownstream.id(),
repeated == null ? null : repeated.id()
);
}
}
assertTrue(changed);
}
@Test
public void optionsRejectNonFiniteOrUnsafeValues() {
assertThrows(IllegalArgumentException.class, () -> options(10L).sourceChance(Double.NaN).build());
@@ -872,6 +1193,9 @@ public class RiverNetworkTest {
assertThrows(IllegalArgumentException.class, () -> options(10L).routingBasinCells(7).build());
assertThrows(IllegalArgumentException.class, () -> options(10L).routingPlateauHeight(0D).build());
assertThrows(IllegalArgumentException.class, () -> options(10L).hydraulicBaseHeight(Double.NaN).build());
assertThrows(IllegalArgumentException.class, () -> options(10L).confluenceWeight(-1D).build());
assertThrows(IllegalArgumentException.class, () -> options(10L).branchSoftCap(0).build());
assertThrows(IllegalArgumentException.class, () -> options(10L).branchChildShrinkFactor(1.1D).build());
assertThrows(IllegalArgumentException.class, () -> options(10L).channelWidth(-1.0).build());
assertThrows(IllegalArgumentException.class, () -> options(10L)
.tileCells(1)
@@ -981,6 +1305,7 @@ public class RiverNetworkTest {
1,
1,
width,
RiverWidthProfile.constant(width),
bankWidth,
3D,
false,
@@ -1022,6 +1347,57 @@ public class RiverNetworkTest {
return hydraulicComparison != 0 ? hydraulicComparison : first.id().compareTo(second.id());
}
private static ReachShape shapeOf(RiverReach reach, double meanderStrength) {
RiverPolyline polyline = reach.polyline();
double deltaX = reach.to().x() - reach.from().x();
double deltaZ = reach.to().z() - reach.from().z();
double chordLength = StrictMath.hypot(deltaX, deltaZ);
double[] offsets = new double[polyline.size()];
double maximumOffset = 0D;
double weightedPosition = 0D;
double totalWeight = 0D;
int crossings = 0;
int previousSign = 0;
for (int point = 0; point < polyline.size(); point++) {
double pointX = polyline.x(point) - reach.from().x();
double pointZ = polyline.z(point) - reach.from().z();
double offset = (pointX * deltaZ - pointZ * deltaX) / chordLength;
offsets[point] = offset;
double absoluteOffset = StrictMath.abs(offset);
maximumOffset = StrictMath.max(maximumOffset, absoluteOffset);
double t = (double) point / (polyline.size() - 1);
weightedPosition += t * absoluteOffset;
totalWeight += absoluteOffset;
int sign = absoluteOffset < meanderStrength * 0.05D ? 0 : offset < 0D ? -1 : 1;
if (sign != 0 && previousSign != 0 && sign != previousSign) {
crossings++;
}
if (sign != 0) {
previousSign = sign;
}
assertTrue(absoluteOffset <= meanderStrength + 0.0000001D);
}
int dominantBand = 1;
double dominantEnergy = -1D;
int samples = offsets.length - 1;
for (int frequency = 1; frequency <= 8; frequency++) {
double cosine = 0D;
double sine = 0D;
for (int point = 0; point < samples; point++) {
double phase = TWO_PI_FOR_TESTS * frequency * point / samples;
cosine += offsets[point] * StrictMath.cos(phase);
sine += offsets[point] * StrictMath.sin(phase);
}
double energy = cosine * cosine + sine * sine;
if (energy > dominantEnergy) {
dominantEnergy = energy;
dominantBand = frequency;
}
}
double centroid = totalWeight <= 0.0000001D ? 0.5D : weightedPosition / totalWeight;
return new ReachShape(crossings, maximumOffset / meanderStrength, centroid, dominantBand);
}
private static long digest(RiverTile tile) {
long hash = 0xCBF29CE484222325L;
for (RiverReach reach : tile.reaches()) {
@@ -1032,6 +1408,10 @@ public class RiverNetworkTest {
hash = RiverNetwork.mix(hash ^ reach.order());
hash = RiverNetwork.mix(hash ^ reach.state().ordinal());
hash = RiverNetwork.mix(hash ^ Double.doubleToLongBits(reach.width()));
for (int index = 0; index < reach.widthProfile().size(); index++) {
hash = RiverNetwork.mix(hash ^ Double.doubleToLongBits(reach.widthProfile().position(index)));
hash = RiverNetwork.mix(hash ^ Double.doubleToLongBits(reach.widthProfile().width(index)));
}
hash = RiverNetwork.mix(hash ^ Double.doubleToLongBits(reach.bankWidth()));
hash = RiverNetwork.mix(hash ^ Double.doubleToLongBits(reach.depth()));
hash = RiverNetwork.mix(hash ^ (reach.mouth() ? 1L : 0L));
@@ -1062,6 +1442,7 @@ public class RiverNetworkTest {
assertEquals(first.flow(), second.flow());
assertEquals(first.order(), second.order());
assertEquals(first.width(), second.width(), 0.0);
assertEquals(first.widthProfile(), second.widthProfile());
assertEquals(first.bankWidth(), second.bankWidth(), 0.0);
assertEquals(first.depth(), second.depth(), 0.0);
assertEquals(first.mouth(), second.mouth());
@@ -1092,4 +1473,7 @@ public class RiverNetworkTest {
return ocean && blockX >= 224;
}
}
private record ReachShape(int crossings, double amplitudeRatio, double centroid, int dominantBand) {
}
}
@@ -295,6 +295,7 @@ public class RiverTileCacheTest {
1,
1,
8.0,
RiverWidthProfile.constant(8.0),
4.0,
3.0,
false,
@@ -160,8 +160,8 @@ public class RiverCaveContainmentPlannerTest {
RiverCavePlan rejected = planner.plan(view, source, rejectedSettings);
assertTrue(accepted.accepted());
assertEquals(RiverCaveAction.DRY_AIR, accepted.actions().get(position(0, 11, 0)));
assertEquals(RiverCaveAction.DRY_AIR, accepted.actions().get(position(0, 12, 0)));
assertEquals(RiverCaveAction.FALLING_WATER, accepted.actions().get(position(0, 11, 0)));
assertEquals(RiverCaveAction.FALLING_WATER, accepted.actions().get(position(0, 12, 0)));
assertEquals(RiverCaveRejection.DRY_HEADROOM_LIMIT, rejected.rejection());
}
@@ -341,7 +341,7 @@ public class RiverCaveContainmentPlannerTest {
RiverCavePlan plan = planner.plan(view, source, settings());
assertTrue(plan.accepted());
assertEquals(RiverCaveAction.DRY_AIR, plan.actions().get(position(0, 12, 0)));
assertEquals(RiverCaveAction.FALLING_WATER, plan.actions().get(position(0, 12, 0)));
assertEquals(RiverCaveAction.WET_SOURCE, plan.actions().get(position(0, 8, 0)));
assertTrue(plan.actions().containsValue(RiverCaveAction.SEAL_GUARD));
assertEquals(plan.actions().keySet(), plan.baselinePreconditions().keySet());
@@ -349,6 +349,44 @@ public class RiverCaveContainmentPlannerTest {
assertMutationConnected(plan, source.entry(), source.target());
}
@Test
public void generatedGrottoAcceptsItsCompleteSurfaceMouth() {
TestVoxelView view = new TestVoxelView();
CavePosition[] wetMouth = {
position(-1, 12, 0),
position(1, 12, 0),
position(0, 12, -1),
position(0, 12, 1)
};
CavePosition[] dryMouth = {
position(-1, 13, 0),
position(1, 13, 0),
position(0, 13, -1),
position(0, 13, 1)
};
view.set(CaveVoxel.COMPATIBLE_FLUID, wetMouth);
view.set(CaveVoxel.CAVE_AIR, dryMouth);
for (CavePosition position : wetMouth) {
view.openToSurface(position);
}
for (CavePosition position : dryMouth) {
view.openToSurface(position);
}
RiverCaveSource source = source(108L, 10, RiverCaveMode.GENERATED_GROTTO, position(0, 8, 0));
RiverCavePlan plan = planner.plan(view, source, detailedSettings(
2,
2,
RiverCaveGrottoShape.ELLIPSOID,
RiverCaveFluidPolicy.ALLOW_COMPATIBLE
));
assertTrue(plan.rejection().toString(), plan.accepted());
for (CavePosition position : wetMouth) {
assertEquals(RiverCaveAction.FALLING_WATER, plan.actions().get(position));
}
}
@Test
public void generatedGrottoRejectsAnOpenShell() {
TestVoxelView view = new TestVoxelView();
@@ -22,7 +22,9 @@ import art.arcane.iris.engine.river.RiverRouteState;
import art.arcane.iris.engine.river.RiverAnchor;
import art.arcane.iris.engine.river.RiverRoutingContext;
import art.arcane.iris.engine.river.RiverTerrainNodeSample;
import art.arcane.iris.engine.river.RiverTopologyComplexity;
import art.arcane.iris.engine.river.RiverTerrainSourceSample;
import art.arcane.iris.engine.river.RiverWidthProfile;
import art.arcane.iris.util.project.interpolation.NoiseBounds;
import art.arcane.iris.util.project.stream.ProceduralStream;
import art.arcane.iris.util.project.stream.interpolation.Interpolated;
@@ -40,6 +42,82 @@ import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
public class IrisRiverRuntimeTest {
@Test
public void tunnelCrossSectionRoundsAndNoiseModulatesBothSurfaces() {
assertEquals(62, IrisRiverRuntime.shapedTunnelBedY(70, 64D, 1D, 2D));
assertEquals(69, IrisRiverRuntime.shapedTunnelBedY(70, 64D, 0D, 2D));
assertEquals(65, IrisRiverRuntime.shapedTunnelBedY(70, 64D, 1D, -1D));
assertEquals(77, IrisRiverRuntime.shapedTunnelCeilingY(70, 4D, 1D, 3D));
assertEquals(70, IrisRiverRuntime.shapedTunnelCeilingY(70, 4D, 0D, 3D));
assertEquals(71, IrisRiverRuntime.shapedTunnelCeilingY(70, 4D, 0.5D, -2D));
}
@Test
public void widenedTunnelHaloAccountsForMaximumConfiguredMultiplier() {
assertEquals(17, RiverTopologyComplexity.tunnelHalo(7D, 4D, 2D));
assertEquals(2500L, RiverTopologyComplexity.tunnelSampleColumns(7D, 4D, 2D));
}
@Test
public void tunnelWidthMultiplierWidensOnlySubterraneanFootprint() {
IrisRiverNetwork narrowConfiguration = configuration(false);
narrowConfiguration.getTerrain()
.setMaxChannelWidth(4D)
.setMaxBankWidth(0D)
.setBankWidth(range(0D))
.setMaxIncision(10)
.setTunnelMouthBlend(0D)
.setTunnelWidthMultiplier(range(1D));
IrisRiverNetwork wideConfiguration = configuration(false);
wideConfiguration.getTerrain()
.setMaxChannelWidth(4D)
.setMaxBankWidth(0D)
.setBankWidth(range(0D))
.setMaxIncision(10)
.setTunnelMouthBlend(0D)
.setTunnelWidthMultiplier(range(4D));
try (IrisRiverRuntime narrow = runtime(
narrowConfiguration,
constantHeight(100D),
constantLandBiome(),
new IrisRegion(),
true,
false
); IrisRiverRuntime wide = runtime(
wideConfiguration,
constantHeight(100D),
constantLandBiome(),
new IrisRegion(),
true,
false
)) {
int narrowTunnelColumns = 0;
int wideTunnelColumns = 0;
int narrowSurfaceColumns = 0;
int wideSurfaceColumns = 0;
for (int x = -128; x < 128; x += 2) {
for (int z = -128; z < 128; z += 2) {
if (narrow.sampleTunnel(x, z) != null) {
narrowTunnelColumns++;
}
if (wide.sampleTunnel(x, z) != null) {
wideTunnelColumns++;
}
if (narrow.sample(x, z).river().present()) {
narrowSurfaceColumns++;
}
if (wide.sample(x, z).river().present()) {
wideSurfaceColumns++;
}
}
}
assertTrue(wideTunnelColumns > narrowTunnelColumns * 2);
assertEquals(narrowSurfaceColumns, wideSurfaceColumns);
}
}
@Test
public void terminalTaperUsesMeasuredReachLength() {
assertEquals(1D, IrisRiverRuntime.terminalWeight(40, 200D, 0.8D), 0D);
@@ -185,6 +263,50 @@ public class IrisRiverRuntimeTest {
}
}
@Test
public void oceanIntentOnlyTerminatesRiversWhereTheNaturalSurfaceIsSubmerged() {
IrisRiverNetwork configuration = configuration(false);
configuration.getTopology().setTerrainHeightWeight(0D);
IrisBiome oceanBiome = new IrisBiome().setInferredType(InferredType.SEA);
IrisRegion region = new IrisRegion();
ProceduralStream<Double> height = ProceduralStream.ofDouble((x, z) -> x < 0D ? 50D : 200D);
ProceduralStream<Boolean> oceanIntent = ProceduralStream.of(
(x, z) -> true,
Interpolated.BOOLEAN
);
ProceduralStream<IrisBiome> biomes = ProceduralStream.of(
(x, z) -> oceanBiome,
Interpolated.of(value -> 0D, value -> oceanBiome)
);
ProceduralStream<IrisRegion> regions = ProceduralStream.of(
(x, z) -> region,
Interpolated.of(value -> 0D, value -> region)
);
try (IrisRiverRuntime runtime = new IrisRiverRuntime(new IrisRiverRuntimeContext(
4829759234L,
configuration,
mock(IrisData.class),
63,
true,
true,
false,
false,
false,
(x, z) -> new NoiseBounds(0D, 512D),
height,
ProceduralStream.ofDouble((x, z) -> 0D),
oceanIntent,
biomes,
regions
))) {
assertTrue(runtime.sampleNode(-1, 0).ocean());
assertFalse(runtime.sampleNode(1, 0).ocean());
assertTrue(runtime.sampleSource(-1, 0).ocean());
assertFalse(runtime.sampleSource(1, 0).ocean());
}
}
@Test
public void wetTerminalRiverCarvesDownAndPublishesAFluidHead() {
IrisRiverNetwork configuration = configuration(false);
@@ -568,6 +690,42 @@ public class IrisRiverRuntimeTest {
}
}
@Test
public void cliffTransitionClassifiesOpenAndSolidColumnsIndependently() {
IrisRiverNetwork configuration = configuration(false);
configuration.getTerrain()
.setMaxIncision(10)
.setTunnelMouthBlend(2D);
ProceduralStream<Double> height = ProceduralStream.ofDouble((x, z) -> x < 0D ? 50D : 100D);
try (IrisRiverRuntime runtime = runtime(
configuration,
height,
constantLandBiome(),
new IrisRegion(),
true,
false
)) {
boolean transitionFound = false;
for (int z = -2048; z <= 2048 && !transitionFound; z++) {
IrisRiverSurfaceSample open = runtime.sample(-1, z);
IrisRiverSurfaceSample solid = runtime.sample(0, z);
IrisRiverTunnelSample tunnel = runtime.sampleTunnel(0, z);
if (open.river().present()
&& solid.river().present()
&& open.river().reachId().equals(solid.river().reachId())
&& open.surfaceFluid()
&& tunnel != null) {
assertFalse(open.subterranean());
assertTrue(solid.subterranean());
transitionFound = true;
}
}
assertTrue(transitionFound);
}
}
@Test
public void terracedWaterUsesFixedInteriorPoolsAndPreservesNodeHeads() {
IrisRiverNetwork configuration = configuration(false);
@@ -635,6 +793,7 @@ public class IrisRiverRuntimeTest {
1,
1,
4D,
RiverWidthProfile.constant(4D),
2D,
4D,
true,
@@ -13,8 +13,10 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
/**
* Severity is stated by the caller and honoured by every adapter, so the levels {@link IrisLogging} offers
@@ -24,6 +26,7 @@ import static org.mockito.Mockito.mock;
public class IrisLoggingTest {
private final List<LogLevel> levels = new ArrayList<>();
private final List<String> messages = new ArrayList<>();
private IrisPlatform capturingPlatform;
private IrisPlatform previousPlatform;
@Before
@@ -55,6 +58,34 @@ public class IrisLoggingTest {
assertEquals("Engine init: world", messages.getFirst());
}
@Test
public void debugFormatsArgumentsAtDebugLevel() {
IrisLogging.debug("chunk=%d,%d", 3, 7);
assertEquals(List.of(LogLevel.DEBUG), levels);
assertEquals("chunk=3,7", messages.getFirst());
}
@Test
public void contextualReportUsesThePlatformReporterWithoutADuplicateLogLine() {
RuntimeException failure = new RuntimeException("broken");
IrisLogging.reportError("Generation failed.", failure);
verify(capturingPlatform).reportError("Generation failed.", failure);
assertTrue(levels.isEmpty());
}
@Test
public void defaultContextualReporterLogsTheFailureContext() {
IrisPlatform platform = mock(IrisPlatform.class, CALLS_REAL_METHODS);
platform.reportError("Generation failed.", null);
verify(platform).log(LogLevel.ERROR, "Generation failed.");
verify(platform).reportError((Throwable) null);
}
@Test
public void warnOnceStatesTheConditionOnceAndDropsRepeatsToDebug() {
String key = uniqueKey();
@@ -104,6 +135,7 @@ public class IrisLoggingTest {
private void bindCapturingPlatform() {
IrisPlatform platform = mock(IrisPlatform.class);
capturingPlatform = platform;
doAnswer(invocation -> {
levels.add(invocation.getArgument(0, LogLevel.class));
messages.add(invocation.getArgument(1, String.class));
@@ -0,0 +1,43 @@
package art.arcane.iris.util.common.scheduling;
import org.junit.After;
import org.junit.Test;
import java.lang.reflect.Field;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class JRepeatingTaskLifecycleTest {
@After
public void clearTrackedTasks() {
trackedTasks().clear();
}
@Test
public void pluginCancellationDrainsEveryTrackedRepeaterOnce() {
AtomicInteger cancellations = new AtomicInteger();
Map<Integer, Runnable> tracked = trackedTasks();
tracked.put(-1, cancellations::incrementAndGet);
tracked.put(-2, cancellations::incrementAndGet);
J.cancelPluginTasks();
J.cancelPluginTasks();
assertEquals(2, cancellations.get());
assertTrue(tracked.isEmpty());
}
@SuppressWarnings("unchecked")
private static Map<Integer, Runnable> trackedTasks() {
try {
Field field = J.class.getDeclaredField("REPEATING_CANCELLERS");
field.setAccessible(true);
return (Map<Integer, Runnable>) field.get(null);
} catch (ReflectiveOperationException failure) {
throw new AssertionError("Unable to inspect tracked Iris repeaters", failure);
}
}
}