diff --git a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructureVolumeIndexTest.java b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructureVolumeIndexTest.java index 84f07324d..50ad1b4c1 100644 --- a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructureVolumeIndexTest.java +++ b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructureVolumeIndexTest.java @@ -32,6 +32,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.LockSupport; import java.util.function.IntConsumer; import static org.junit.Assert.assertEquals; @@ -218,7 +219,7 @@ public class NativeStructureVolumeIndexTest { if (cleared) { break; } - Thread.sleep(1); + LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(10L)); } assertTrue("Origin eviction did not complete before the query cache lock", cleared); resolver.release(); diff --git a/core/src/test/java/art/arcane/iris/core/SettingsHotloadWatchTest.java b/core/src/test/java/art/arcane/iris/core/SettingsHotloadWatchTest.java index af1f73860..8547a18ce 100644 --- a/core/src/test/java/art/arcane/iris/core/SettingsHotloadWatchTest.java +++ b/core/src/test/java/art/arcane/iris/core/SettingsHotloadWatchTest.java @@ -2,8 +2,10 @@ package art.arcane.iris.core; import art.arcane.iris.core.localization.IrisLanguage; import art.arcane.iris.core.localization.IrisMessages; +import art.arcane.iris.testsupport.Await; import art.arcane.volmlib.util.hotload.ConfigHotloadEngine; import art.arcane.volmlib.util.localization.MessageArgument; +import java.time.Duration; import org.junit.After; import org.junit.Before; import org.junit.Rule; @@ -213,12 +215,12 @@ public class SettingsHotloadWatchTest { assertEquals(1, automaticApplies.get()); Files.writeString(override.toPath(), locale("en_US", "Manual {permission}"), StandardCharsets.UTF_8); - pollFor(125L); + pollWithoutChange(125L, "First " + PERMISSION); assertEquals("First " + PERMISSION, permissionMessage()); assertTrue(IrisLanguage.reload(dataFolder, "en_US")); assertEquals("Manual " + PERMISSION, permissionMessage()); - pollFor(650L); + pollWithoutChange(650L, "Manual " + PERMISSION); assertEquals(1, automaticApplies.get()); assertEquals("Manual " + PERMISSION, permissionMessage()); } @@ -254,15 +256,11 @@ public class SettingsHotloadWatchTest { assertEquals("Active " + PERMISSION, permissionMessage()); } - private void awaitPermissionMessage(String expected) throws Exception { - long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(6L); - while (System.nanoTime() < deadline) { + private void awaitPermissionMessage(String expected) { + Await.reached("the hotload permission message '" + expected + "'", Duration.ofSeconds(6L), () -> { watch.checkConfigHotload(); - if (expected.equals(permissionMessage())) { - return; - } - Thread.sleep(25L); - } + return expected.equals(permissionMessage()); + }); assertEquals(expected, permissionMessage()); } @@ -275,31 +273,24 @@ public class SettingsHotloadWatchTest { } private void checkUntilEntered(CountDownLatch entered) { - try { - while (entered.getCount() > 0L) { - watch.checkConfigHotload(); - Thread.sleep(10L); - } - } catch (InterruptedException failure) { - Thread.currentThread().interrupt(); - throw new IllegalStateException("Interrupted while waiting for a hotload snapshot", failure); - } + Await.until("the hotload apply boundary", Duration.ofSeconds(5L), () -> { + watch.checkConfigHotload(); + return entered.getCount() == 0L; + }); } - private void awaitThreadState(Thread thread, Thread.State expected) throws InterruptedException { - long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(1L); - while (System.nanoTime() < deadline && thread.getState() != expected) { - Thread.sleep(5L); - } + private void awaitThreadState(Thread thread, Thread.State expected) { + Await.reached(thread.getName() + " to reach " + expected, Duration.ofSeconds(1L), + () -> thread.getState() == expected); assertEquals(expected, thread.getState()); } - private void pollFor(long durationMillis) throws InterruptedException { - long deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(durationMillis); - while (System.nanoTime() < deadline) { - watch.checkConfigHotload(); - Thread.sleep(25L); - } + private void pollWithoutChange(long windowMillis, String expected) { + assertFalse("The hotload permission message changed inside the quiet window", + Await.reached("an unexpected hotload apply", Duration.ofMillis(windowMillis), () -> { + watch.checkConfigHotload(); + return !expected.equals(permissionMessage()); + })); } private String captureErrorsWhilePolling(String expected, long duplicateWindowMillis) throws Exception { @@ -307,20 +298,17 @@ public class SettingsHotloadWatchTest { ByteArrayOutputStream captured = new ByteArrayOutputStream(); try (PrintStream capture = new PrintStream(captured, true, StandardCharsets.UTF_8)) { System.setErr(capture); - long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(4L); - while (!captured.toString(StandardCharsets.UTF_8).contains(expected)) { - if (System.nanoTime() >= deadline) { - fail("Timed out after 4000ms waiting for the hotload diagnostic '" + expected - + "'; captured stderr was: " + captured.toString(StandardCharsets.UTF_8)); - } + if (!Await.reached("the hotload diagnostic '" + expected + "'", Duration.ofSeconds(4L), () -> { watch.checkConfigHotload(); - Thread.sleep(10L); + return captured.toString(StandardCharsets.UTF_8).contains(expected); + })) { + fail("Timed out after 4000ms waiting for the hotload diagnostic '" + expected + + "'; captured stderr was: " + captured.toString(StandardCharsets.UTF_8)); } - long duplicateDeadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(duplicateWindowMillis); - while (System.nanoTime() < duplicateDeadline) { + Await.reached("a duplicate hotload diagnostic", Duration.ofMillis(duplicateWindowMillis), () -> { watch.checkConfigHotload(); - Thread.sleep(10L); - } + return false; + }); } finally { System.setErr(originalError); } diff --git a/core/src/test/java/art/arcane/iris/core/gui/NoiseRenderCoordinatorTest.java b/core/src/test/java/art/arcane/iris/core/gui/NoiseRenderCoordinatorTest.java index c24d40edc..437e330c1 100644 --- a/core/src/test/java/art/arcane/iris/core/gui/NoiseRenderCoordinatorTest.java +++ b/core/src/test/java/art/arcane/iris/core/gui/NoiseRenderCoordinatorTest.java @@ -133,6 +133,8 @@ public final class NoiseRenderCoordinatorTest { AtomicReference failure = new AtomicReference<>(); AtomicReference samplingThread = new AtomicReference<>(); AtomicBoolean sampledOnEdt = new AtomicBoolean(); + AtomicBoolean renderFinished = new AtomicBoolean(); + CountDownLatch sampledAfterCompletion = new CountDownLatch(1); AtomicInteger samples = new AtomicInteger(); NoiseRenderCoordinator coordinator = new NoiseRenderCoordinator(2, new NoiseRenderCoordinator.Listener() { @Override @@ -154,6 +156,9 @@ public final class NoiseRenderCoordinatorTest { try { NoiseProvider sampler = (x, z) -> { samples.incrementAndGet(); + if (renderFinished.get()) { + sampledAfterCompletion.countDown(); + } samplingThread.compareAndSet(null, Thread.currentThread().getName()); sampledOnEdt.compareAndSet(false, SwingUtilities.isEventDispatchThread()); return x + z; @@ -177,7 +182,8 @@ public final class NoiseRenderCoordinatorTest { assertFalse(sampledOnEdt.get()); assertTrue(samplingThread.get().startsWith("Iris Noise Renderer")); int completedSamples = samples.get(); - Thread.sleep(150L); + renderFinished.set(true); + assertFalse(sampledAfterCompletion.await(150L, TimeUnit.MILLISECONDS)); assertEquals(completedSamples, samples.get()); } finally { coordinator.close(); diff --git a/core/src/test/java/art/arcane/iris/core/gui/VisionRenderControllerTest.java b/core/src/test/java/art/arcane/iris/core/gui/VisionRenderControllerTest.java index 14e63055e..ef4dbe6c2 100644 --- a/core/src/test/java/art/arcane/iris/core/gui/VisionRenderControllerTest.java +++ b/core/src/test/java/art/arcane/iris/core/gui/VisionRenderControllerTest.java @@ -2,9 +2,11 @@ package art.arcane.iris.core.gui; import art.arcane.iris.engine.framework.render.IrisRenderer; import art.arcane.iris.engine.framework.render.RenderType; +import art.arcane.iris.testsupport.Await; import org.junit.Test; import java.awt.image.BufferedImage; +import java.time.Duration; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -86,7 +88,8 @@ public class VisionRenderControllerTest { VisionRenderController.RenderSpec spec = spec(renderer, RenderType.BIOME, 1L, 0D, 0D, 4D, 128, 128); try { VisionRenderController.Frame first = controller.request(spec); - assertTrue(await(() -> controller.progress(first).ready() == controller.progress(first).total(), 2_000L)); + Await.until("the first render to report every tile ready", Duration.ofSeconds(2L), + () -> controller.progress(first).ready() == controller.progress(first).total()); int rendered = calls.get(); VisionRenderController.Frame cached = controller.request(spec); @@ -108,7 +111,8 @@ public class VisionRenderControllerTest { VisionRenderController controller = controller(2); try { VisionRenderController.Frame first = controller.request(spec(renderer, RenderType.BIOME, 1L, 16D, 16D, 4D, 128, 128)); - assertTrue(await(() -> controller.progress(first).ready() == controller.progress(first).total(), 2_000L)); + Await.until("the first render to report every tile ready", Duration.ofSeconds(2L), + () -> controller.progress(first).ready() == controller.progress(first).total()); Set firstKeys = keys(first); VisionRenderController.Frame panned = controller.request(spec(renderer, RenderType.BIOME, 1L, 20D, 20D, 4D, 128, 128)); @@ -249,15 +253,4 @@ public class VisionRenderControllerTest { height ); } - - private static boolean await(BooleanSupplier condition, long timeoutMillis) throws InterruptedException { - long deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(timeoutMillis); - while (System.nanoTime() < deadline) { - if (condition.getAsBoolean()) { - return true; - } - Thread.sleep(5L); - } - return condition.getAsBoolean(); - } } diff --git a/core/src/test/java/art/arcane/iris/core/pack/PackDownloadExecutionTest.java b/core/src/test/java/art/arcane/iris/core/pack/PackDownloadExecutionTest.java index 79426f240..8d321a75b 100644 --- a/core/src/test/java/art/arcane/iris/core/pack/PackDownloadExecutionTest.java +++ b/core/src/test/java/art/arcane/iris/core/pack/PackDownloadExecutionTest.java @@ -61,11 +61,12 @@ public class PackDownloadExecutionTest { public void cancellationInterruptsRunningWorkOutsidePublication() throws Exception { LifecycleOperationCoordinator.Lease lease = mock(LifecycleOperationCoordinator.Lease.class); CountDownLatch started = new CountDownLatch(1); + CountDownLatch neverReleased = new CountDownLatch(1); AtomicBoolean interrupted = new AtomicBoolean(); PackDownloadExecution execution = new PackDownloadExecution(lease, cancellation -> { started.countDown(); try { - Thread.sleep(TimeUnit.SECONDS.toMillis(30L)); + neverReleased.await(30L, TimeUnit.SECONDS); } catch (InterruptedException exception) { interrupted.set(true); Thread.currentThread().interrupt(); diff --git a/core/src/test/java/art/arcane/iris/engine/IrisEngineGenerationRuntimeScopeTest.java b/core/src/test/java/art/arcane/iris/engine/IrisEngineGenerationRuntimeScopeTest.java index af5ffa731..5124af86e 100644 --- a/core/src/test/java/art/arcane/iris/engine/IrisEngineGenerationRuntimeScopeTest.java +++ b/core/src/test/java/art/arcane/iris/engine/IrisEngineGenerationRuntimeScopeTest.java @@ -7,6 +7,8 @@ import art.arcane.iris.engine.framework.BiomeEnvironment; import art.arcane.iris.engine.history.SavedBiomeRuntime; import art.arcane.iris.engine.history.SavedBiomeUnavailableException; import art.arcane.iris.engine.object.IrisRegion; +import art.arcane.iris.testsupport.Await; +import art.arcane.iris.testsupport.PlatformBinding; import java.util.Optional; import java.util.concurrent.TimeUnit; import art.arcane.iris.engine.framework.EngineMode; @@ -27,23 +29,19 @@ import art.arcane.iris.engine.object.IrisDimensionCarvingEntry; import art.arcane.iris.engine.object.IrisDimensionCarvingResolver; import art.arcane.iris.engine.object.IrisRange; import art.arcane.iris.engine.object.IrisWorld; -import art.arcane.iris.spi.IrisPlatform; -import art.arcane.iris.spi.IrisPlatforms; -import art.arcane.iris.spi.PlatformBlockState; -import art.arcane.iris.spi.PlatformRegistries; import art.arcane.iris.util.common.parallel.MultiBurst; import art.arcane.iris.util.project.context.ChunkContext; import art.arcane.iris.util.project.context.IrisContext; import art.arcane.iris.util.project.noise.CNG; import art.arcane.iris.util.project.stream.ProceduralStream; import art.arcane.volmlib.util.collection.KList; +import org.junit.ClassRule; import org.junit.Test; -import org.junit.AfterClass; -import org.junit.BeforeClass; import java.io.IOException; import java.lang.reflect.Field; import java.nio.file.Path; +import java.time.Duration; import java.util.Collections; import java.util.IdentityHashMap; import java.util.Map; @@ -65,7 +63,6 @@ import static org.junit.Assert.assertSame; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.CALLS_REAL_METHODS; -import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doCallRealMethod; @@ -78,21 +75,8 @@ import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; public class IrisEngineGenerationRuntimeScopeTest { - @BeforeClass - public static void bindPlatform() { - IrisPlatforms.unbind(); - PlatformBlockState block = mock(PlatformBlockState.class); - PlatformRegistries registries = mock(PlatformRegistries.class); - when(registries.block(anyString())).thenReturn(block); - IrisPlatform platform = mock(IrisPlatform.class); - when(platform.registries()).thenReturn(registries); - IrisPlatforms.bind(platform); - } - - @AfterClass - public static void unbindPlatform() { - IrisPlatforms.unbind(); - } + @ClassRule + public static final PlatformBinding PLATFORM = PlatformBinding.mockPlatform(); @Test public void carvingResolverRestoresNestedRuntimeDefinitions() throws Exception { @@ -878,11 +862,9 @@ public class IrisEngineGenerationRuntimeScopeTest { return router; } - private static void awaitBlocked(Thread thread) throws InterruptedException { - long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5L); - while (thread.getState() != Thread.State.BLOCKED && thread.isAlive() && System.nanoTime() < deadline) { - Thread.sleep(1L); - } + private static void awaitBlocked(Thread thread) { + Await.reached("the reader to block on the engine lifecycle lock", Duration.ofSeconds(5L), + () -> thread.getState() == Thread.State.BLOCKED || !thread.isAlive()); assertEquals(Thread.State.BLOCKED, thread.getState()); } diff --git a/core/src/test/java/art/arcane/iris/engine/WorldChunkMaintenanceRoutingTest.java b/core/src/test/java/art/arcane/iris/engine/WorldChunkMaintenanceRoutingTest.java index 16d8e2fdd..5882becd5 100644 --- a/core/src/test/java/art/arcane/iris/engine/WorldChunkMaintenanceRoutingTest.java +++ b/core/src/test/java/art/arcane/iris/engine/WorldChunkMaintenanceRoutingTest.java @@ -6,10 +6,8 @@ import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter.SavedChunkM import art.arcane.iris.engine.mantle.EngineMantle; import art.arcane.iris.engine.platform.EngineBukkitOps; import art.arcane.iris.spi.IrisLogging; -import art.arcane.iris.spi.IrisPlatform; -import art.arcane.iris.spi.IrisPlatforms; -import art.arcane.iris.spi.PlatformBlockState; -import art.arcane.iris.spi.PlatformRegistries; +import art.arcane.iris.testsupport.Await; +import art.arcane.iris.testsupport.PlatformBinding; import art.arcane.iris.util.common.scheduling.J; import art.arcane.volmlib.util.mantle.flag.MantleFlag; import art.arcane.volmlib.util.mantle.runtime.Mantle; @@ -17,13 +15,13 @@ import art.arcane.volmlib.util.mantle.runtime.MantleChunk; import art.arcane.volmlib.util.matter.Matter; import org.bukkit.Chunk; import org.bukkit.World; +import org.junit.ClassRule; import org.junit.Test; -import org.junit.AfterClass; -import org.junit.BeforeClass; import org.mockito.InOrder; import org.mockito.MockedStatic; import java.lang.reflect.Field; +import java.time.Duration; import java.util.ArrayDeque; import java.util.Map; import java.util.Optional; @@ -52,26 +50,9 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; public class WorldChunkMaintenanceRoutingTest { - private static IrisPlatform previousPlatform; + @ClassRule + public static final PlatformBinding PLATFORM = PlatformBinding.mockPlatform(); - @BeforeClass - public static void bindPlatform() { - previousPlatform = IrisPlatforms.isBound() ? IrisPlatforms.get() : null; - IrisPlatforms.unbind(); - IrisPlatform platform = mock(IrisPlatform.class); - PlatformRegistries registries = mock(PlatformRegistries.class); - when(platform.registries()).thenReturn(registries); - when(registries.block(anyString())).thenReturn(mock(PlatformBlockState.class)); - IrisPlatforms.bind(platform); - } - - @AfterClass - public static void restorePlatform() { - IrisPlatforms.unbind(); - if (previousPlatform != null) { - IrisPlatforms.bind(previousPlatform); - } - } @Test public void materializesTheSavedOwnerAndKeepsSpawningAfterCompletion() throws Exception { @@ -372,10 +353,7 @@ public class WorldChunkMaintenanceRoutingTest { private void awaitRelease() throws InterruptedException { assertTrue("Saved mantle handle was not released", released.tryAcquire(5L, TimeUnit.SECONDS)); - long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5L); - while (!pending.isEmpty() && System.nanoTime() < deadline) { - Thread.sleep(1L); - } + Await.reached("the saved mantle cleanup to finish", Duration.ofSeconds(5L), pending::isEmpty); assertTrue("Saved mantle cleanup did not finish", pending.isEmpty()); } diff --git a/core/src/test/java/art/arcane/iris/engine/history/GenerationAdmissionTest.java b/core/src/test/java/art/arcane/iris/engine/history/GenerationAdmissionTest.java index 4ce8a7b10..08754886e 100644 --- a/core/src/test/java/art/arcane/iris/engine/history/GenerationAdmissionTest.java +++ b/core/src/test/java/art/arcane/iris/engine/history/GenerationAdmissionTest.java @@ -1,10 +1,12 @@ package art.arcane.iris.engine.history; +import art.arcane.iris.testsupport.Await; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; import java.nio.file.Path; +import java.time.Duration; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CompletableFuture; @@ -14,6 +16,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.FutureTask; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import static org.junit.Assert.assertEquals; @@ -38,7 +41,7 @@ public class GenerationAdmissionTest { }); assertTrue(requested.await(5L, TimeUnit.SECONDS)); - Thread.sleep(100L); + assertThrows(TimeoutException.class, () -> cutover.get(100L, TimeUnit.MILLISECONDS)); assertFalse(cutover.isDone()); stage.close(); cutover.get(5L, TimeUnit.SECONDS); @@ -246,12 +249,14 @@ public class GenerationAdmissionTest { } } - private static void awaitWaiting(Thread thread) throws InterruptedException { - long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5L); - while (thread.getState() != Thread.State.WAITING && System.nanoTime() < deadline) { + private static void awaitWaiting(Thread thread) { + Await.reached("the generation operation to reach the admission gate", Duration.ofSeconds(5L), () -> { + if (thread.getState() == Thread.State.WAITING) { + return true; + } assertTrue("Generation operation completed before reaching the admission gate.", thread.isAlive()); - Thread.sleep(1L); - } + return false; + }); assertEquals("Generation operation did not reach the admission gate.", Thread.State.WAITING, thread.getState()); } diff --git a/core/src/test/java/art/arcane/iris/engine/history/GenerationHistoryAdmissionTest.java b/core/src/test/java/art/arcane/iris/engine/history/GenerationHistoryAdmissionTest.java index fb75e2fac..e3f8d4bd2 100644 --- a/core/src/test/java/art/arcane/iris/engine/history/GenerationHistoryAdmissionTest.java +++ b/core/src/test/java/art/arcane/iris/engine/history/GenerationHistoryAdmissionTest.java @@ -1,5 +1,6 @@ package art.arcane.iris.engine.history; +import art.arcane.iris.testsupport.Await; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; @@ -13,6 +14,7 @@ import java.nio.channels.FileChannel; import java.nio.ByteBuffer; import java.nio.file.Files; import java.nio.file.Path; +import java.time.Duration; import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -251,10 +253,8 @@ public class GenerationHistoryAdmissionTest { Field queueField = GenerationHistory.class.getDeclaredField("pendingSemanticClaims"); queueField.setAccessible(true); ArrayBlockingQueue queue = (ArrayBlockingQueue) queueField.get(history); - long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5L); - while (queue.remainingCapacity() != 0 && System.nanoTime() < deadline) { - Thread.sleep(1L); - } + Await.reached("the claim queue to reach capacity", Duration.ofSeconds(5L), + () -> queue.remainingCapacity() == 0); assertEquals(0, queue.remainingCapacity()); assertEquals(128, queue.size()); } @@ -354,24 +354,21 @@ public class GenerationHistoryAdmissionTest { } } - private static void awaitBlockedOnHistory(GenerationHistory history, List workers, int count) - throws InterruptedException { - long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5L); - while (System.nanoTime() < deadline) { - int blocked = 0; - for (Thread worker : workers) { - ThreadInfo info = ManagementFactory.getThreadMXBean().getThreadInfo(worker.threadId()); - if (info != null && info.getThreadState() == Thread.State.BLOCKED - && info.getLockInfo().getIdentityHashCode() == System.identityHashCode(history)) { - blocked++; - } + private static void awaitBlockedOnHistory(GenerationHistory history, List workers, int count) { + Await.until("semantic claim callers to queue behind the History monitor", Duration.ofSeconds(5L), + () -> blockedOnHistory(history, workers) == count); + } + + private static int blockedOnHistory(GenerationHistory history, List workers) { + int blocked = 0; + for (Thread worker : workers) { + ThreadInfo info = ManagementFactory.getThreadMXBean().getThreadInfo(worker.threadId()); + if (info != null && info.getThreadState() == Thread.State.BLOCKED + && info.getLockInfo().getIdentityHashCode() == System.identityHashCode(history)) { + blocked++; } - if (blocked == count) { - return; - } - Thread.sleep(1L); } - throw new AssertionError("Semantic claim callers did not queue behind the History monitor"); + return blocked; } private static boolean persistPaused(GenerationHistory history, GenerationHistory.GenerationStage stage, diff --git a/core/src/test/java/art/arcane/iris/engine/history/SavedBiomeRuntimeTest.java b/core/src/test/java/art/arcane/iris/engine/history/SavedBiomeRuntimeTest.java index 5e357ab0b..ca821d85e 100644 --- a/core/src/test/java/art/arcane/iris/engine/history/SavedBiomeRuntimeTest.java +++ b/core/src/test/java/art/arcane/iris/engine/history/SavedBiomeRuntimeTest.java @@ -5,8 +5,11 @@ import art.arcane.iris.engine.IrisEngine; import art.arcane.iris.engine.framework.BiomeEnvironment; import art.arcane.iris.engine.framework.PreservationRegistry; import art.arcane.iris.spi.IrisServices; +import art.arcane.iris.testsupport.Await; +import art.arcane.iris.testsupport.PlatformLeakGuard; import org.junit.After; import org.junit.Before; +import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; @@ -15,6 +18,7 @@ import java.io.IOException; import java.lang.reflect.Field; import java.nio.file.Files; import java.nio.file.Path; +import java.time.Duration; import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -39,6 +43,9 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; public class SavedBiomeRuntimeTest { + @ClassRule + public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean(); + @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); @@ -263,10 +270,8 @@ public class SavedBiomeRuntimeTest { consumption.writeLock().lock(); Future rendered = caller.submit(() -> runtime.readSurfaceBiome(0, 0, Optional::isEmpty)); try { - long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5L); - while (runtime.cachedQueryCount() == 0 && System.nanoTime() < deadline) { - Thread.sleep(1L); - } + Await.reached("the background preview to cache its query", Duration.ofSeconds(5L), + () -> runtime.cachedQueryCount() != 0); assertEquals(1, runtime.cachedQueryCount()); for (int chunkX = 1; chunkX <= 130; chunkX++) { int requestedX = chunkX; @@ -451,11 +456,9 @@ public class SavedBiomeRuntimeTest { when(history.semantics(anyInt(), anyInt())).thenReturn(Optional.empty()); } - private static void awaitIdle(SavedBiomeRuntime runtime) throws InterruptedException { - long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5L); - while (runtime.pendingQueryCount() != 0 && System.nanoTime() < deadline) { - Thread.sleep(1L); - } + private static void awaitIdle(SavedBiomeRuntime runtime) { + Await.reached("the runtime to drain its pending queries", Duration.ofSeconds(5L), + () -> runtime.pendingQueryCount() == 0); assertEquals(0, runtime.pendingQueryCount()); } diff --git a/core/src/test/java/art/arcane/iris/engine/history/SavedBiomeStoreBatchTest.java b/core/src/test/java/art/arcane/iris/engine/history/SavedBiomeStoreBatchTest.java index 99da9c38a..b40a2ef7a 100644 --- a/core/src/test/java/art/arcane/iris/engine/history/SavedBiomeStoreBatchTest.java +++ b/core/src/test/java/art/arcane/iris/engine/history/SavedBiomeStoreBatchTest.java @@ -1,5 +1,6 @@ package art.arcane.iris.engine.history; +import art.arcane.iris.testsupport.Await; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; @@ -11,6 +12,7 @@ import java.lang.reflect.Field; import java.nio.channels.FileChannel; import java.nio.file.Files; import java.nio.file.Path; +import java.time.Duration; import java.util.ArrayList; import java.util.List; import java.util.Queue; @@ -207,10 +209,8 @@ public class SavedBiomeStoreBatchTest { for (SavedBiomeChunk chunk : chunks) { results.add(workers.submit(() -> claim(store, root, control, chunk))); } - long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5); - while (pending.size() != chunks.size() && System.nanoTime() < deadline) { - Thread.sleep(1L); - } + Await.reached("every claim to queue behind the region write", Duration.ofSeconds(5L), + () -> pending.size() == chunks.size()); assertEquals(chunks.size(), pending.size()); } finally { writingField.setBoolean(stripe, false); diff --git a/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyTileCacheTest.java b/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyTileCacheTest.java index 0827fbe5c..d8f194306 100644 --- a/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyTileCacheTest.java +++ b/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyTileCacheTest.java @@ -1,10 +1,12 @@ package art.arcane.iris.engine.hydrology; +import art.arcane.iris.testsupport.Await; import art.arcane.iris.util.common.parallel.MultiBurst; import art.arcane.volmlib.util.cache.CacheKey; import org.junit.Test; import java.lang.reflect.Field; +import java.time.Duration; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -137,10 +139,7 @@ public class HydrologyTileCacheTest { Field field = HydrologyTileCache.class.getDeclaredField("closed"); field.setAccessible(true); AtomicBoolean closed = (AtomicBoolean) field.get(cache); - long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5); - while (!closed.get() && System.nanoTime() < deadline) { - Thread.sleep(1); - } + Await.reached("the cache to finish closing", Duration.ofSeconds(5L), closed::get); assertTrue(closed.get()); } @@ -1162,22 +1161,13 @@ public class HydrologyTileCacheTest { } } - private static void awaitConcurrentPlan(AtomicInteger inFlight, long boundMillis) throws InterruptedException { - long deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(boundMillis); - while (inFlight.get() < 2 && System.nanoTime() < deadline) { - Thread.sleep(1L); - } + private static void awaitConcurrentPlan(AtomicInteger inFlight, long boundMillis) { + Await.reached("a second plan to run concurrently", Duration.ofMillis(boundMillis), () -> inFlight.get() >= 2); } - private static void awaitParkedCallers(List callers, int expected) throws InterruptedException { - long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5); - while (System.nanoTime() < deadline) { - if (callers.size() == expected && allParked(callers)) { - return; - } - Thread.sleep(1L); - } - throw new AssertionError("Callers did not park on the in-flight plan"); + private static void awaitParkedCallers(List callers, int expected) { + Await.until("callers to park on the in-flight plan", Duration.ofSeconds(5L), + () -> callers.size() == expected && allParked(callers)); } private static boolean allParked(List callers) { diff --git a/core/src/test/java/art/arcane/iris/engine/mantle/MatterGeneratorConcurrencyTest.java b/core/src/test/java/art/arcane/iris/engine/mantle/MatterGeneratorConcurrencyTest.java index 16df4d316..7e9b6488a 100644 --- a/core/src/test/java/art/arcane/iris/engine/mantle/MatterGeneratorConcurrencyTest.java +++ b/core/src/test/java/art/arcane/iris/engine/mantle/MatterGeneratorConcurrencyTest.java @@ -3,10 +3,8 @@ package art.arcane.iris.engine.mantle; import art.arcane.iris.core.IrisSettings; import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.object.IrisDimension; -import art.arcane.iris.spi.IrisPlatform; -import art.arcane.iris.spi.IrisPlatforms; -import art.arcane.iris.spi.PlatformBlockState; -import art.arcane.iris.spi.PlatformRegistries; +import art.arcane.iris.testsupport.Await; +import art.arcane.iris.testsupport.PlatformBinding; import art.arcane.iris.util.project.context.ChunkContext; import art.arcane.iris.util.project.context.IrisContext; import art.arcane.volmlib.util.mantle.flag.MantleFlag; @@ -16,9 +14,11 @@ import art.arcane.volmlib.util.mantle.runtime.MantleChunk; import art.arcane.volmlib.util.matter.Matter; import org.junit.AfterClass; import org.junit.BeforeClass; +import org.junit.ClassRule; import org.junit.Test; import art.arcane.iris.util.common.parallel.MultiBurst; +import java.time.Duration; import java.util.List; import java.util.Set; import java.util.concurrent.CancellationException; @@ -43,7 +43,6 @@ import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -52,24 +51,19 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; public class MatterGeneratorConcurrencyTest { + @ClassRule + public static final PlatformBinding PLATFORM = PlatformBinding.mockPlatform(); + private static IrisSettings previousSettings; @BeforeClass public static void bindPlatform() { previousSettings = IrisSettings.settings; IrisSettings.settings = new IrisSettings(); - IrisPlatforms.unbind(); - PlatformBlockState defaultBlock = mock(PlatformBlockState.class); - PlatformRegistries registries = mock(PlatformRegistries.class); - IrisPlatform platform = mock(IrisPlatform.class); - when(registries.block(anyString())).thenReturn(defaultBlock); - when(platform.registries()).thenReturn(registries); - IrisPlatforms.bind(platform); } @AfterClass public static void unbindPlatform() { - IrisPlatforms.unbind(); IrisSettings.settings = previousSettings; } @@ -592,10 +586,8 @@ public class MatterGeneratorConcurrencyTest { return null; }); - long deadline = System.currentTimeMillis() + 10_000L; - while (secondCompleted.size() < 7 && System.currentTimeMillis() < deadline) { - Thread.sleep(10L); - } + Await.reached("the second generation to finish its own seven chunks", Duration.ofSeconds(10L), + () -> secondCompleted.size() >= 7); // The second generation's window is x 1..3, z -1..1. The first generation finished (1,-1) // before parking on (1,0), so the second one owns the remaining seven chunks and must // complete all of them while (1,0) is still held instead of blocking on it first. diff --git a/core/src/test/java/art/arcane/iris/engine/object/IrisDimensionCarvingResolverParityTest.java b/core/src/test/java/art/arcane/iris/engine/object/IrisDimensionCarvingResolverParityTest.java index 58e3e5151..9701c0ae5 100644 --- a/core/src/test/java/art/arcane/iris/engine/object/IrisDimensionCarvingResolverParityTest.java +++ b/core/src/test/java/art/arcane/iris/engine/object/IrisDimensionCarvingResolverParityTest.java @@ -4,6 +4,7 @@ import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.core.loader.ResourceLoader; import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.SeedManager; +import art.arcane.iris.testsupport.Await; import art.arcane.iris.testsupport.BukkitTestServer; import art.arcane.iris.util.project.noise.CNG; import art.arcane.volmlib.util.collection.KList; @@ -14,6 +15,7 @@ import org.mockito.invocation.InvocationOnMock; import java.lang.ref.WeakReference; import java.lang.reflect.Field; import java.lang.reflect.Proxy; +import java.time.Duration; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -22,6 +24,7 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import static org.junit.Assert.assertFalse; @@ -281,17 +284,18 @@ public class IrisDimensionCarvingResolverParityTest { new WeakReference<>(fixture.engine())); } - private static void awaitCollection(LifetimeReferences references) throws InterruptedException { - for (int attempt = 0; attempt < 100; attempt++) { + private static void awaitCollection(LifetimeReferences references) { + AtomicInteger attempt = new AtomicInteger(); + Await.reached("the thread-local State and Engine to be collected", Duration.ofSeconds(5L), () -> { System.gc(); if (references.state().get() == null && references.engine().get() == null) { - return; + return true; } byte[] pressure = new byte[1_048_576]; - pressure[0] = (byte) attempt; - Thread.sleep(10L); - } + pressure[0] = (byte) attempt.incrementAndGet(); + return false; + }); assertTrue("Thread-local State was retained", references.state().get() == null); assertTrue("Engine was retained", references.engine().get() == null); } diff --git a/core/src/test/java/art/arcane/iris/testsupport/Await.java b/core/src/test/java/art/arcane/iris/testsupport/Await.java new file mode 100644 index 000000000..d64aeb6d4 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/testsupport/Await.java @@ -0,0 +1,41 @@ +package art.arcane.iris.testsupport; + +import java.time.Duration; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.LockSupport; +import java.util.function.BooleanSupplier; + +public final class Await { + private static final long POLL_INTERVAL_NANOS = TimeUnit.MILLISECONDS.toNanos(10L); + private static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(5L); + + private Await() { + } + + public static void until(String what, BooleanSupplier condition) { + until(what, DEFAULT_TIMEOUT, condition); + } + + public static void until(String what, Duration timeout, BooleanSupplier condition) { + if (!reached(what, timeout, condition)) { + throw new AssertionError("Timed out after " + timeout.toMillis() + "ms waiting for " + what); + } + } + + public static boolean reached(String what, Duration timeout, BooleanSupplier condition) { + long deadline = System.nanoTime() + timeout.toNanos(); + while (true) { + if (condition.getAsBoolean()) { + return true; + } + if (System.nanoTime() - deadline >= 0L) { + return false; + } + LockSupport.parkNanos(POLL_INTERVAL_NANOS); + if (Thread.interrupted()) { + Thread.currentThread().interrupt(); + throw new AssertionError("Interrupted while waiting for " + what); + } + } + } +}