From a6149b27c9c2d0addee27fa72b9774b5b31ca99c Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Sun, 4 Oct 2026 13:08:41 -0400 Subject: [PATCH] Adapter patches (tests) --- .../IrisChunkGeneratorHeightCacheTest.java | 6 + .../nativegen/NativeTransitionColumnTest.java | 6 + .../nativegen/v26_3_R1/NativeBlockTags.java | 17 ++ .../WorldgenTerrainHeightmapsTest.java | 6 + .../iris/probe/BareMobSpawnHistoryTest.java | 4 + .../probe/HeadlessChunkGeneratorTest.java | 4 + .../iris/probe/HeadlessChunkLightingTest.java | 4 + .../probe/HeadlessChunkSerializationTest.java | 4 + .../probe/HeadlessNativeBootstrapTest.java | 4 + .../probe/HeadlessNativeRegistriesTest.java | 4 + .../probe/HeadlessNativeRegistryTest.java | 4 + .../iris/probe/HeadlessNativeTileTest.java | 4 + .../iris/probe/HeadlessRegionTerrainTest.java | 4 + .../probe/NativeRegionTerrainWriterTest.java | 4 + .../OfflineNativeFeatureFeasibilityTest.java | 4 + .../probe/OfflineRegionPublicationTest.java | 4 + .../iris/probe/PaperRegistryIsolation.java | 37 +++++ build.gradle | 19 ++- core/build.gradle | 4 - .../decoration/IrisOreBandsTest.java | 75 ++++++--- .../hydrology/HydrologyPlanHashTest.java | 46 ++++-- .../mantle/CaveTerrainSnapshotTest.java | 3 +- .../IrisCaveCarver3DNearParityTest.java | 87 +++++----- .../runtime/EngineBiomeColumnTest.java | 104 +++++++----- .../IrisDepositModifierOrderingTest.java | 34 ++-- .../stage/IrisDepositModifierParityTest.java | 30 ++-- .../IrisPerfectionModifierColumnTest.java | 156 ++++++++++++++++-- ...risDimensionCarvingResolverParityTest.java | 8 +- .../PregeneratorJobTerminalProgressTest.java | 27 ++- .../world/history/SavedBiomeCaptureTest.java | 38 +++-- .../world/history/SavedBiomeRuntimeTest.java | 11 +- .../runtime/ProbeBackgroundTasks.java | 17 ++ .../iris/probe/GenerationOrderProbe.java | 2 +- .../arcane/iris/probe/HydrologyPackProbe.java | 4 +- .../iris/probe/RealPackProbeSupport.java | 82 ++++++++- .../art/arcane/iris/probe/Terrain3DProbe.java | 2 +- .../arcane/iris/probe/TransitionProbe.java | 2 +- .../runtime/ProbeBackgroundTasksTest.java | 131 +++++++++++++++ .../probe/ObjectContinuationProbeTest.java | 2 +- .../RealPackProbeSupportBarrierTest.java | 101 ++++++++++++ 40 files changed, 894 insertions(+), 211 deletions(-) create mode 100644 adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/probe/PaperRegistryIsolation.java create mode 100644 probe/src/main/java/art/arcane/iris/generation/runtime/ProbeBackgroundTasks.java create mode 100644 probe/src/test/java/art/arcane/iris/generation/runtime/ProbeBackgroundTasksTest.java create mode 100644 probe/src/test/java/art/arcane/iris/probe/RealPackProbeSupportBarrierTest.java diff --git a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/IrisChunkGeneratorHeightCacheTest.java b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/IrisChunkGeneratorHeightCacheTest.java index 48243703a..2be925c3a 100644 --- a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/IrisChunkGeneratorHeightCacheTest.java +++ b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/IrisChunkGeneratorHeightCacheTest.java @@ -14,6 +14,7 @@ import net.minecraft.SharedConstants; import net.minecraft.server.Bootstrap; import net.minecraft.world.level.LevelHeightAccessor; import net.minecraft.world.level.levelgen.Heightmap; +import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; @@ -45,6 +46,11 @@ public final class IrisChunkGeneratorHeightCacheTest { NativeBlockTags.bindHeightmapFixtures(); } + @AfterClass + public static void restoreBlockTags() { + NativeBlockTags.restoreTags(); + } + @Test public void repeatedNativeHeightUsesLiveHeightQueriesAcrossRuntimeChanges() throws Exception { Fixture fixture = fixture(); diff --git a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeTransitionColumnTest.java b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeTransitionColumnTest.java index e4317a803..9aec00001 100644 --- a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeTransitionColumnTest.java +++ b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/NativeTransitionColumnTest.java @@ -20,6 +20,7 @@ import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.RotatedPillarBlock; import net.minecraft.world.level.block.LiquidBlock; import net.minecraft.world.level.levelgen.Heightmap; +import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; @@ -46,6 +47,11 @@ public final class NativeTransitionColumnTest { NativeBlockTags.bindHeightmapFixtures(); } + @AfterClass + public static void restoreBlockTags() { + NativeBlockTags.restoreTags(); + } + @Test public void customKeysUseCarrierForNativeColumnsAndHeightmaps() { String key = "itemsadder:rocks/ruby_ore"; diff --git a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/v26_3_R1/NativeBlockTags.java b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/v26_3_R1/NativeBlockTags.java index 441028018..26886c4ce 100644 --- a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/v26_3_R1/NativeBlockTags.java +++ b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/nativegen/v26_3_R1/NativeBlockTags.java @@ -1,21 +1,30 @@ package art.arcane.iris.nativegen.v26_3_R1; import net.minecraft.core.Holder; +import net.minecraft.core.HolderSet; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.core.registries.Registries; import net.minecraft.tags.BlockTags; import net.minecraft.tags.TagLoader; +import net.minecraft.tags.TagKey; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.Blocks; +import java.util.HashMap; import java.util.List; import java.util.Map; public final class NativeBlockTags { + private static Map, List>> originalTags; + private NativeBlockTags() { } public static void bindHeightmapFixtures() { + originalTags = new HashMap<>(); + for (HolderSet.Named tag : BuiltInRegistries.BLOCK.getTags().toList()) { + originalTags.put(tag.key(), tag.stream().toList()); + } List> solid = List.of(Blocks.STONE.builtInRegistryHolder(), Blocks.OAK_LOG.builtInRegistryHolder(), Blocks.SPRUCE_LOG.builtInRegistryHolder()); List> canopy = List.of(Blocks.STONE.builtInRegistryHolder(), @@ -25,4 +34,12 @@ public final class NativeBlockTags { BlockTags.BLOCKS_MOTION_IN_HEIGHTMAP, canopy, BlockTags.BLOCKS_MOTION_IN_HEIGHTMAP_NO_LEAVES, solid))).apply(); } + + public static void restoreTags() { + if (originalTags == null) { + return; + } + BuiltInRegistries.BLOCK.prepareTagReload(new TagLoader.LoadResult<>(Registries.BLOCK, originalTags)).apply(); + originalTags = null; + } } diff --git a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/WorldgenTerrainHeightmapsTest.java b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/WorldgenTerrainHeightmapsTest.java index 50bcaad1e..8bf655771 100644 --- a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/WorldgenTerrainHeightmapsTest.java +++ b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/platform/bukkit/nms/v26_3_R1/WorldgenTerrainHeightmapsTest.java @@ -34,6 +34,7 @@ import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer; import net.minecraft.world.level.levelgen.structure.structures.IglooPieces; import net.minecraft.world.level.levelgen.structure.structures.SwampHutPiece; import net.minecraft.world.level.levelgen.structure.structures.SwampHutStructure; +import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; @@ -68,6 +69,11 @@ public class WorldgenTerrainHeightmapsTest { NativeBlockTags.bindHeightmapFixtures(); } + @AfterClass + public static void restoreBlockTags() { + NativeBlockTags.restoreTags(); + } + @Test public void authoringHeightmapsReadPlacedObjectsAndFluidsWithoutTerrainResolvers() { ProtoChunk chunk = terrainChunk(new ChunkPos(0, 0)); diff --git a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/probe/BareMobSpawnHistoryTest.java b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/probe/BareMobSpawnHistoryTest.java index 2adc1b3e7..74ca7d45d 100644 --- a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/probe/BareMobSpawnHistoryTest.java +++ b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/probe/BareMobSpawnHistoryTest.java @@ -13,6 +13,7 @@ import com.google.gson.JsonParser; import org.bukkit.Bukkit; import org.junit.AfterClass; import org.junit.BeforeClass; +import org.junit.ClassRule; import org.junit.Test; import java.io.File; @@ -26,6 +27,9 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThrows; public final class BareMobSpawnHistoryTest { + @ClassRule + public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation(); + private static final String LEGACY_RENDERER_IDENTITY = "bukkit-v26_3_R1-generated-registry-json-v1"; private static final String NATURAL_MOB_SPAWNS = "minecraft:gameplay/natural_mob_spawns"; private static final GenerationRegistryContract.PhysicalResourceKey INSTALLED = diff --git a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/probe/HeadlessChunkGeneratorTest.java b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/probe/HeadlessChunkGeneratorTest.java index 092b0fe9c..038279345 100644 --- a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/probe/HeadlessChunkGeneratorTest.java +++ b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/probe/HeadlessChunkGeneratorTest.java @@ -29,6 +29,7 @@ import net.minecraft.world.level.levelgen.structure.StructureStart; import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager; import net.minecraft.world.level.storage.LevelStorageSource; import org.junit.Rule; +import org.junit.ClassRule; import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.bukkit.craftbukkit.generator.CustomChunkGenerator; @@ -49,6 +50,9 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; public class HeadlessChunkGeneratorTest { + @ClassRule + public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation(); + @Rule public TemporaryFolder temporary = new TemporaryFolder(); diff --git a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/probe/HeadlessChunkLightingTest.java b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/probe/HeadlessChunkLightingTest.java index f379416ff..70ccb78e5 100644 --- a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/probe/HeadlessChunkLightingTest.java +++ b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/probe/HeadlessChunkLightingTest.java @@ -31,6 +31,7 @@ import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemp import net.minecraft.world.level.storage.LevelStorageSource; import org.bukkit.Bukkit; import org.junit.Rule; +import org.junit.ClassRule; import org.junit.Test; import org.junit.rules.TemporaryFolder; @@ -48,6 +49,9 @@ import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; public final class HeadlessChunkLightingTest { + @ClassRule + public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation(); + @Rule public TemporaryFolder temporary = new TemporaryFolder(); diff --git a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/probe/HeadlessChunkSerializationTest.java b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/probe/HeadlessChunkSerializationTest.java index ecf33662b..64ef115ac 100644 --- a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/probe/HeadlessChunkSerializationTest.java +++ b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/probe/HeadlessChunkSerializationTest.java @@ -38,6 +38,7 @@ import net.minecraft.world.ticks.ScheduledTick; import org.bukkit.NamespacedKey; import org.bukkit.persistence.PersistentDataType; import org.junit.Rule; +import org.junit.ClassRule; import org.junit.Test; import org.junit.rules.TemporaryFolder; @@ -54,6 +55,9 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; public class HeadlessChunkSerializationTest { + @ClassRule + public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation(); + @Rule public TemporaryFolder temporary = new TemporaryFolder(); diff --git a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/probe/HeadlessNativeBootstrapTest.java b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/probe/HeadlessNativeBootstrapTest.java index 5eddc8968..8607d0215 100644 --- a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/probe/HeadlessNativeBootstrapTest.java +++ b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/probe/HeadlessNativeBootstrapTest.java @@ -9,6 +9,7 @@ import net.minecraft.world.item.Items; import java.util.Map; import net.minecraft.world.level.block.Blocks; +import org.junit.ClassRule; import org.junit.Test; import org.bukkit.Bukkit; @@ -18,6 +19,9 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; public final class HeadlessNativeBootstrapTest { + @ClassRule + public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation(); + @Test public void bindsNativeItemComponentsWithoutStartingServer() throws Exception { HeadlessNativeBootstrap.bindComponents(HeadlessNativeTestRegistries.get()); diff --git a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/probe/HeadlessNativeRegistriesTest.java b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/probe/HeadlessNativeRegistriesTest.java index 0f5b1bf57..34b845b8d 100644 --- a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/probe/HeadlessNativeRegistriesTest.java +++ b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/probe/HeadlessNativeRegistriesTest.java @@ -7,12 +7,16 @@ import net.minecraft.resources.Identifier; import net.minecraft.resources.ResourceKey; import net.minecraft.world.level.biome.Biome; import net.minecraft.world.level.levelgen.structure.Structure; +import org.junit.ClassRule; import org.junit.Test; import static org.junit.Assert.assertTrue; public final class HeadlessNativeRegistriesTest { + @ClassRule + public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation(); + @Test public void nativeStructureBiomeTagsUseTheLoadedBiomeHolders() throws Exception { RegistryAccess registries = HeadlessNativeTestRegistries.get(); diff --git a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/probe/HeadlessNativeRegistryTest.java b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/probe/HeadlessNativeRegistryTest.java index 6ecc541d9..0190af6d5 100644 --- a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/probe/HeadlessNativeRegistryTest.java +++ b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/probe/HeadlessNativeRegistryTest.java @@ -4,6 +4,7 @@ import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeRegistryAccess; import net.minecraft.core.registries.Registries; import org.bukkit.Bukkit; import org.junit.BeforeClass; +import org.junit.ClassRule; import org.junit.Test; import java.util.Map; @@ -17,6 +18,9 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThrows; public final class HeadlessNativeRegistryTest { + @ClassRule + public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation(); + @BeforeClass public static void bootstrap() { HeadlessNativeBootstrap.initialize(); diff --git a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/probe/HeadlessNativeTileTest.java b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/probe/HeadlessNativeTileTest.java index 78eb64c05..4644c129e 100644 --- a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/probe/HeadlessNativeTileTest.java +++ b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/probe/HeadlessNativeTileTest.java @@ -11,6 +11,7 @@ import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeTileReader; import net.minecraft.nbt.CompoundTag; import net.minecraft.data.registries.VanillaRegistries; import org.junit.BeforeClass; +import org.junit.ClassRule; import org.junit.Test; import java.io.ByteArrayInputStream; @@ -23,6 +24,9 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThrows; public final class HeadlessNativeTileTest { + @ClassRule + public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation(); + @BeforeClass public static void bootstrap() { HeadlessNativeBootstrap.initialize(); diff --git a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/probe/HeadlessRegionTerrainTest.java b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/probe/HeadlessRegionTerrainTest.java index 421b91373..ea155b6b9 100644 --- a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/probe/HeadlessRegionTerrainTest.java +++ b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/probe/HeadlessRegionTerrainTest.java @@ -17,6 +17,7 @@ import org.bukkit.Bukkit; import org.bukkit.NamespacedKey; import org.bukkit.persistence.PersistentDataType; import org.junit.Rule; +import org.junit.ClassRule; import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.spigotmc.SpigotWorldConfig; @@ -36,6 +37,9 @@ import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; public final class HeadlessRegionTerrainTest { + @ClassRule + public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation(); + @Rule public TemporaryFolder temporary = new TemporaryFolder(); diff --git a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/probe/NativeRegionTerrainWriterTest.java b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/probe/NativeRegionTerrainWriterTest.java index 36d905729..0d1fd0c0f 100644 --- a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/probe/NativeRegionTerrainWriterTest.java +++ b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/probe/NativeRegionTerrainWriterTest.java @@ -24,6 +24,7 @@ import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.chunk.storage.RegionFile; import net.minecraft.world.level.chunk.storage.RegionStorageInfo; import org.junit.BeforeClass; +import org.junit.ClassRule; import org.junit.Test; import java.io.DataInputStream; @@ -38,6 +39,9 @@ import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; public final class NativeRegionTerrainWriterTest { + @ClassRule + public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation(); + @BeforeClass public static void bootstrap() { SharedConstants.tryDetectVersion(); diff --git a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/probe/OfflineNativeFeatureFeasibilityTest.java b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/probe/OfflineNativeFeatureFeasibilityTest.java index b40093d09..80c71a675 100644 --- a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/probe/OfflineNativeFeatureFeasibilityTest.java +++ b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/probe/OfflineNativeFeatureFeasibilityTest.java @@ -26,6 +26,7 @@ import net.minecraft.world.level.material.Fluids; import org.bukkit.Bukkit; import org.junit.AfterClass; import org.junit.BeforeClass; +import org.junit.ClassRule; import org.junit.Test; import java.util.ArrayList; @@ -40,6 +41,9 @@ import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; public final class OfflineNativeFeatureFeasibilityTest { + @ClassRule + public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation(); + private static HeadlessNativeRegistries loaded; private static RegistryAccess registries; private static DimensionType dimension; diff --git a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/probe/OfflineRegionPublicationTest.java b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/probe/OfflineRegionPublicationTest.java index f6f4bb09f..e1bc3b473 100644 --- a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/probe/OfflineRegionPublicationTest.java +++ b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/probe/OfflineRegionPublicationTest.java @@ -1,6 +1,7 @@ package art.arcane.iris.probe; import org.junit.Rule; +import org.junit.ClassRule; import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.mockito.MockedStatic; @@ -22,6 +23,9 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; public final class OfflineRegionPublicationTest { + @ClassRule + public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation(); + @Rule public TemporaryFolder temporary = new TemporaryFolder(); diff --git a/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/probe/PaperRegistryIsolation.java b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/probe/PaperRegistryIsolation.java new file mode 100644 index 000000000..557da823c --- /dev/null +++ b/adapters/bukkit/nms/v26_3_R1/src/test/java/art/arcane/iris/probe/PaperRegistryIsolation.java @@ -0,0 +1,37 @@ +package art.arcane.iris.probe; + +import io.papermc.paper.registry.PaperRegistryAccess; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Map; + +public final class PaperRegistryIsolation implements TestRule { + @Override + public Statement apply(Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + Map registries = registries(); + Map previous = new HashMap<>(registries); + registries.clear(); + try { + base.evaluate(); + } finally { + registries.clear(); + registries.putAll(previous); + } + } + }; + } + + @SuppressWarnings("unchecked") + private static Map registries() throws ReflectiveOperationException { + Field field = PaperRegistryAccess.class.getDeclaredField("registries"); + field.setAccessible(true); + return (Map) field.get(PaperRegistryAccess.instance()); + } +} diff --git a/build.gradle b/build.gradle index 9decb4507..a61b0dae1 100644 --- a/build.gradle +++ b/build.gradle @@ -96,7 +96,7 @@ String forgeArtifactName = irisArtifactName('Forge', "${minecraftVersion}+${load String neoForgeArtifactName = irisArtifactName('NeoForge', "${minecraftVersion}+${loaderDisplayVersion(neoForgeVersion)}") Map runtimeDownloadRelocations = (Map) ext.runtimeDownloadRelocations List runtimeDownloadedPrefixes = (List) ext.runtimeDownloadedPrefixes -File pluginOutDirectory = layout.projectDirectory.dir('../PluginOuts').asFile +File pluginOutDirectory = file(providers.gradleProperty('pluginOutDirectory').getOrElse('../PluginOuts')) File legacyBukkitJar = layout.buildDirectory.file("libs/Iris-${project.version}.jar").get().asFile File bukkitArtifact = layout.buildDirectory.file("libs/${bukkitArtifactName}").get().asFile File consumerBukkitArtifact = layout.buildDirectory.file("consumer/${bukkitArtifactName}").get().asFile @@ -327,6 +327,9 @@ if (localVolmLibCheckout != null) { // moves. Without a local checkout the Exec tasks always run and the nested build decides what is stale. Closure declareVolmLibInputs = { Exec task -> task.inputs.property('volmLibStamp', volmLibStamp) + String nativeBillow = providers.gradleProperty('includeNativeBillow').getOrElse('false') + task.inputs.property('includeNativeBillow', nativeBillow) + task.environment('ORG_GRADLE_PROJECT_includeNativeBillow', nativeBillow) if (localVolmLibCheckout == null) { task.outputs.upToDateWhen { false } } @@ -781,9 +784,20 @@ tasks.register('irisDev', Copy) { dependsOn(':core:javadocJar') } +int defaultTestForks = Math.min(8, Math.max(1, Runtime.runtime.availableProcessors().intdiv(2))) +int testForks = Math.max(1, Integer.parseInt(providers.gradleProperty('testForks') + .getOrElse(String.valueOf(defaultTestForks)))) + allprojects { apply plugin: 'java' + tasks.withType(Test).configureEach { + int moduleForks = project.path.startsWith(':adapters:bukkit:') ? Math.min(2, testForks) + : project.path == ':probe' ? Math.min(4, testForks) : testForks + maxParallelForks = Math.min(gradle.startParameter.maxWorkerCount, moduleForks) + jvmArgs('-XX:ParallelGCThreads=2', '-XX:ConcGCThreads=1', '-XX:CICompilerCount=2') + } + java { toolchain { languageVersion = JavaLanguageVersion.of(25) @@ -979,9 +993,6 @@ project(':adapters:bukkit:nms:v26_3_R1') { } sourceSets.test.compileClasspath += sourceSets.offline.output sourceSets.test.runtimeClasspath += sourceSets.offline.output - tasks.named('test', Test).configure { - forkEvery = 1 - } TaskProvider offlineJar = tasks.register('offlineJar', Jar) { dependsOn(offlineClasses) diff --git a/core/build.gradle b/core/build.gradle index edc046f0c..5580b0c22 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -170,12 +170,8 @@ dependencies { testImplementation(libs.multiverseCore) } -int coreTestForks = Math.max(1, Integer.parseInt(providers.gradleProperty('testForks') - .getOrElse(String.valueOf(Math.max(1, (int) (Runtime.runtime.availableProcessors() / 2)))))) - tasks.named('test').configure { maxHeapSize = '1g' - maxParallelForks = coreTestForks jvmArgs('-XX:+ExitOnOutOfMemoryError') } diff --git a/core/src/test/java/art/arcane/iris/generation/decoration/IrisOreBandsTest.java b/core/src/test/java/art/arcane/iris/generation/decoration/IrisOreBandsTest.java index afe6ccba6..aab4c31f2 100644 --- a/core/src/test/java/art/arcane/iris/generation/decoration/IrisOreBandsTest.java +++ b/core/src/test/java/art/arcane/iris/generation/decoration/IrisOreBandsTest.java @@ -1,6 +1,8 @@ package art.arcane.iris.generation.decoration; import art.arcane.iris.generation.block.IrisBlockData; +import art.arcane.iris.pack.loading.IrisData; +import art.arcane.iris.testsupport.KeyedBlockState; import art.arcane.iris.generation.noise.IrisGeneratorStyle; import art.arcane.iris.generation.noise.NoiseStyle; import art.arcane.iris.generation.terrain.IrisMaterialPalette; @@ -17,11 +19,6 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyDouble; -import static org.mockito.ArgumentMatchers.isNull; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; public class IrisOreBandsTest { private static final RNG RNG_SOURCE = new RNG(77L); @@ -69,10 +66,8 @@ public class IrisOreBandsTest { @Test public void constantChanceMatchesSamplingTheFlatField() { IrisOreGenerator flat = new IrisOreGenerator(); - IrisMaterialPalette palette = mock(IrisMaterialPalette.class); - NativeBlockState ore = mock(NativeBlockState.class); - when(palette.getPalette()).thenReturn(new KList<>(mock(IrisBlockData.class))); - when(palette.get(any(), anyDouble(), anyDouble(), anyDouble(), isNull())).thenReturn(ore); + NativeBlockState ore = new KeyedBlockState("minecraft:iron_ore"); + IrisMaterialPalette palette = new FixedPalette(ore); IrisGeneratorStyle style = new IrisGeneratorStyle(NoiseStyle.FLAT).setZoom(2.5D).setExponent(1.3D) .setFracture(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).setMultiplier(9D)); flat.setPalette(palette).setChanceStyle(style).setRange(new IrisRange(0, 100)); @@ -101,20 +96,9 @@ public class IrisOreBandsTest { private static IrisOreGenerator generator(double min, double max, double threshold, long seed) { IrisOreGenerator generator = new IrisOreGenerator(); - IrisMaterialPalette palette = mock(IrisMaterialPalette.class); - NativeBlockState state = mock(NativeBlockState.class); - when(palette.getPalette()).thenReturn(new KList<>(mock(IrisBlockData.class))); - when(palette.get(any(), anyDouble(), anyDouble(), anyDouble(), isNull())).thenReturn(state); - CNG chance = mock(CNG.class); - when(chance.noise(anyDouble(), anyDouble(), anyDouble())).thenAnswer(invocation -> { - long hash = seed; - hash = hash * 31 + Double.doubleToLongBits(invocation.getArgument(0)); - hash = hash * 31 + Double.doubleToLongBits(invocation.getArgument(1)); - hash = hash * 31 + Double.doubleToLongBits(invocation.getArgument(2)); - return new Random(hash).nextDouble(); - }); - IrisGeneratorStyle style = mock(IrisGeneratorStyle.class); - when(style.create(any(), isNull())).thenReturn(chance); + NativeBlockState state = new KeyedBlockState("minecraft:iron_ore"); + IrisMaterialPalette palette = new FixedPalette(state); + IrisGeneratorStyle style = new FixedChanceStyle(new CoordinateChance(seed)); return generator.setPalette(palette).setChanceStyle(style).setThreshold(threshold) .setRange(new IrisRange(min, max)); } @@ -131,4 +115,49 @@ public class IrisOreBandsTest { assertSame(scan(ores, 0, 58, 0), bands.generate(0, 58, 0, RNG_SOURCE, null)); assertNull(bands.contains(59) ? bands.generate(0, 59, 0, RNG_SOURCE, null) : null); } + private static final class FixedPalette extends IrisMaterialPalette { + private final NativeBlockState state; + + private FixedPalette(NativeBlockState state) { + this.state = state; + setPalette(new KList<>(new IrisBlockData())); + } + + @Override + public NativeBlockState get(RNG rng, double x, double y, double z, IrisData data) { + return state; + } + } + + private static final class FixedChanceStyle extends IrisGeneratorStyle { + private final CNG chance; + + private FixedChanceStyle(CNG chance) { + this.chance = chance; + } + + @Override + public CNG create(RNG rng, IrisData data) { + return chance; + } + } + + private static final class CoordinateChance extends CNG { + private final long seed; + + private CoordinateChance(long seed) { + super(new RNG(seed)); + this.seed = seed; + } + + @Override + public double noise(double x, double y, double z) { + long hash = seed; + hash = hash * 31 + Double.doubleToLongBits(x); + hash = hash * 31 + Double.doubleToLongBits(y); + hash = hash * 31 + Double.doubleToLongBits(z); + return new Random(hash).nextDouble(); + } + } + } diff --git a/core/src/test/java/art/arcane/iris/generation/hydrology/HydrologyPlanHashTest.java b/core/src/test/java/art/arcane/iris/generation/hydrology/HydrologyPlanHashTest.java index ea9987859..2eeb2a939 100644 --- a/core/src/test/java/art/arcane/iris/generation/hydrology/HydrologyPlanHashTest.java +++ b/core/src/test/java/art/arcane/iris/generation/hydrology/HydrologyPlanHashTest.java @@ -11,6 +11,7 @@ import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Comparator; import java.util.List; +import java.util.HexFormat; import java.util.Map; import static org.junit.Assert.assertEquals; @@ -27,7 +28,7 @@ public class HydrologyPlanHashTest { @Test public void fullTilePlansAreByteIdentical() { - StringBuilder canonical = new StringBuilder(); + MessageDigest canonical = sha256(); int courses = 0; int columns = 0; for (long seed : new long[]{77L, 19L}) { @@ -37,16 +38,16 @@ public class HydrologyPlanHashTest { HydrologyTile tile = planner.plan(key); courses += tile.courses().size(); columns += tile.footprint().columns().size(); - canonical.append(render(tile)); + render(tile, canonical); } } assertTrue("planned courses: " + courses, courses > 0); assertTrue("planned columns: " + columns, columns > 0); - assertEquals(EXPECTED_PLAN_DIGEST, digest(canonical.toString())); + assertEquals(EXPECTED_PLAN_DIGEST, HexFormat.of().formatHex(canonical.digest())); } - private static String render(HydrologyTile tile) { - StringBuilder out = new StringBuilder(); + private static void render(HydrologyTile tile, MessageDigest digest) { + CanonicalDigest out = new CanonicalDigest(digest); out.append("tile ").append(tile.key()).append('\n'); out.append("seed ").append(tile.worldSeed()).append('\n'); out.append("fingerprint ").append(tile.settingsFingerprint()).append('\n'); @@ -78,23 +79,14 @@ public class HydrologyPlanHashTest { for (Map.Entry column : columns) { out.append("column ").append(column.getKey()).append(' ').append(column.getValue()).append('\n'); } - return out.toString(); } - private static String digest(String value) { - MessageDigest sha256; + private static MessageDigest sha256() { try { - sha256 = MessageDigest.getInstance("SHA-256"); - } catch (NoSuchAlgorithmException e) { - throw new IllegalStateException(e); + return MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException failure) { + throw new IllegalStateException(failure); } - byte[] hash = sha256.digest(value.getBytes(StandardCharsets.UTF_8)); - StringBuilder hex = new StringBuilder(hash.length * 2); - for (byte b : hash) { - hex.append(Character.forDigit((b >> 4) & 0xF, 16)); - hex.append(Character.forDigit(b & 0xF, 16)); - } - return hex.toString(); } private static HydrologyTerrainSampler rollingCoast(int coastX) { @@ -230,4 +222,22 @@ public class HydrologyPlanHashTest { HydrologyPlannerSettings.SurfacePolicyBounds.NONE ); } + + private static final class CanonicalDigest { + private final MessageDigest digest; + + private CanonicalDigest(MessageDigest digest) { + this.digest = digest; + } + + private CanonicalDigest append(Object value) { + digest.update(String.valueOf(value).getBytes(StandardCharsets.UTF_8)); + return this; + } + + private CanonicalDigest append(char value) { + digest.update((byte) value); + return this; + } + } } diff --git a/core/src/test/java/art/arcane/iris/generation/mantle/CaveTerrainSnapshotTest.java b/core/src/test/java/art/arcane/iris/generation/mantle/CaveTerrainSnapshotTest.java index a9150b137..8712b3082 100644 --- a/core/src/test/java/art/arcane/iris/generation/mantle/CaveTerrainSnapshotTest.java +++ b/core/src/test/java/art/arcane/iris/generation/mantle/CaveTerrainSnapshotTest.java @@ -23,6 +23,7 @@ import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import static org.mockito.Mockito.withSettings; public class CaveTerrainSnapshotTest { private static final int SECTIONS = 5; @@ -176,7 +177,7 @@ public class CaveTerrainSnapshotTest { @SuppressWarnings("unchecked") private static MantleChunk chunk(Matter[] sections) { - MantleChunk chunk = mock(MantleChunk.class); + MantleChunk chunk = mock(MantleChunk.class, withSettings().stubOnly()); when(chunk.sectionCount()).thenReturn(sections.length); when(chunk.exists(anyInt())).thenAnswer(call -> { int section = call.getArgument(0); diff --git a/core/src/test/java/art/arcane/iris/generation/mantle/IrisCaveCarver3DNearParityTest.java b/core/src/test/java/art/arcane/iris/generation/mantle/IrisCaveCarver3DNearParityTest.java index 2bb001781..e6963e27c 100644 --- a/core/src/test/java/art/arcane/iris/generation/mantle/IrisCaveCarver3DNearParityTest.java +++ b/core/src/test/java/art/arcane/iris/generation/mantle/IrisCaveCarver3DNearParityTest.java @@ -20,6 +20,7 @@ import art.arcane.volmlib.util.math.RNG; import art.arcane.volmlib.util.matter.Matter; import art.arcane.volmlib.util.matter.MatterCavern; import art.arcane.volmlib.util.matter.MatterSlice; +import art.arcane.volmlib.util.matter.slices.CavernMatter; import org.junit.BeforeClass; import org.junit.Test; @@ -52,6 +53,7 @@ import static org.mockito.ArgumentMatchers.anyDouble; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.withSettings; public class IrisCaveCarver3DNearParityTest { private static final long CARVE_SEED = -54_777_863_784_492_918L; @@ -136,7 +138,7 @@ public class IrisCaveCarver3DNearParityTest { createProfile(false, false).setDetailWeight(0D).setAllowFluid(false)); AtomicInteger calls = new AtomicInteger(); Set sampledY = new HashSet<>(); - CNG density = mock(CNG.class); + CNG density = mock(CNG.class, withSettings().stubOnly()); doAnswer(invocation -> { calls.incrementAndGet(); double x = invocation.getArgument(0); @@ -193,14 +195,14 @@ public class IrisCaveCarver3DNearParityTest { @Test public void expressionStylesKeepTheOriginalSamplingAndCarvingBehavior() throws Exception { - CNG density = mock(CNG.class); + CNG density = mock(CNG.class, withSettings().stubOnly()); AtomicInteger calls = new AtomicInteger(); doAnswer(invocation -> { calls.incrementAndGet(); double y = invocation.getArgument(1); return Math.sin(y * 0.2D); }).when(density).noiseFastSigned3D(anyDouble(), anyDouble(), anyDouble()); - IrisGeneratorStyle style = mock(IrisGeneratorStyle.class); + IrisGeneratorStyle style = mock(IrisGeneratorStyle.class, withSettings().stubOnly()); doReturn("density_expression").when(style).getExpression(); doReturn(density).when(style).create(any(RNG.class), any(IrisData.class)); IrisCaveProfile profile = createProfile(false, false).setBaseDensityStyle(style) @@ -240,10 +242,10 @@ public class IrisCaveCarver3DNearParityTest { .setModules(new KList<>(new IrisCaveFieldModule(new IrisGeneratorStyle(NoiseStyle.FLAT), 1D, 0D, new IrisRange(5D, 6D), false))); IrisCaveCarver3D carver = new IrisCaveCarver3D(engine, profile); - CNG base = mock(CNG.class); + CNG base = mock(CNG.class, withSettings().stubOnly()); doReturn(1D).when(base).noiseFastSigned3D(anyDouble(), anyDouble(), anyDouble()); baseDensityField.set(carver, base); - CNG module = mock(CNG.class); + CNG module = mock(CNG.class, withSettings().stubOnly()); doReturn(-1D).when(module).noiseFastSigned3D(anyDouble(), anyDouble(), anyDouble()); moduleDensityField.set(((CaveFieldModuleState[]) modulesField.get(carver))[0], module); WriterCapture capture = createWriterCapture(128); @@ -307,7 +309,7 @@ public class IrisCaveCarver3DNearParityTest { WriterCapture capture = createWriterCapture(128); if (fail) { AtomicInteger samples = new AtomicInteger(); - CNG density = mock(CNG.class); + CNG density = mock(CNG.class, withSettings().stubOnly()); doAnswer(invocation -> { if (samples.incrementAndGet() > 300) { throw new IllegalStateException("Density sample failed"); @@ -380,7 +382,7 @@ public class IrisCaveCarver3DNearParityTest { .setDetailWeight(0D) .setDensityThreshold(new IrisStyledRange(-2D, -2D, new IrisGeneratorStyle(NoiseStyle.FLAT))); IrisCaveCarver3D carver = new IrisCaveCarver3D(engine, profile); - CNG detail = mock(CNG.class); + CNG detail = mock(CNG.class, withSettings().stubOnly()); doAnswer(invocation -> { throw new AssertionError("Rejected voxel sampled an aquifer"); }).when(detail).noiseFastSigned3D(anyDouble(), anyDouble(), anyDouble()); @@ -1343,10 +1345,10 @@ public class IrisCaveCarver3DNearParityTest { } private Engine createEngine(int worldHeight, int sampledHeight) { - Engine engine = mock(Engine.class); - IrisData data = mock(IrisData.class); - IrisDimension dimension = mock(IrisDimension.class); - SeedManager seedManager = mock(SeedManager.class); + Engine engine = mock(Engine.class, withSettings().stubOnly()); + IrisData data = mock(IrisData.class, withSettings().stubOnly()); + IrisDimension dimension = mock(IrisDimension.class, withSettings().stubOnly()); + SeedManager seedManager = mock(SeedManager.class, withSettings().stubOnly()); EngineMetrics metrics = new EngineMetrics(16); IrisWorld world = IrisWorld.builder().minHeight(0).maxHeight(worldHeight).build(); @@ -1467,15 +1469,15 @@ public class IrisCaveCarver3DNearParityTest { } private WriterCapture createWriterCapture(int worldHeight) { - MantleWriter writer = mock(MantleWriter.class); + MantleWriter writer = mock(MantleWriter.class, withSettings().stubOnly()); @SuppressWarnings("unchecked") - Mantle mantle = mock(Mantle.class); + Mantle mantle = mock(Mantle.class, withSettings().stubOnly()); @SuppressWarnings("unchecked") - MantleChunk chunk = mock(MantleChunk.class); + MantleChunk chunk = mock(MantleChunk.class, withSettings().stubOnly()); Map sections = new HashMap<>(); - Map> sectionCells = new HashMap<>(); Set carvedCells = new HashSet<>(); Map carvedLiquids = new HashMap<>(); + WriterCapture capture = new WriterCapture(writer, carvedCells, carvedLiquids); doReturn(mantle).when(writer).getMantle(); doReturn(worldHeight).when(mantle).getWorldHeight(); @@ -1488,41 +1490,19 @@ public class IrisCaveCarver3DNearParityTest { return section; } - Matter created = createSection(sectionIndex, sectionCells, carvedCells, carvedLiquids); + Matter created = createSection(sectionIndex, capture); sections.put(sectionIndex, created); return created; }).when(chunk).getOrCreate(anyInt()); - return new WriterCapture(writer, carvedCells, carvedLiquids); + return capture; } - private Matter createSection(int sectionIndex, Map> sectionCells, Set carvedCells, Map carvedLiquids) { - Matter matter = mock(Matter.class); - @SuppressWarnings("unchecked") - MatterSlice slice = mock(MatterSlice.class); - Map localCells = sectionCells.computeIfAbsent(sectionIndex, key -> new HashMap<>()); - + private Matter createSection(int sectionIndex, WriterCapture capture) { + Matter matter = mock(Matter.class, withSettings().stubOnly()); + MatterSlice slice = new CapturingCavernMatter(sectionIndex, capture); doReturn(slice).when(matter).slice(MatterCavern.class); doReturn(slice).when(matter).getSlice(MatterCavern.class); - doAnswer(invocation -> { - int localX = invocation.getArgument(0); - int localY = invocation.getArgument(1); - int localZ = invocation.getArgument(2); - return localCells.get(packLocal(localX, localY, localZ)); - }).when(slice).get(anyInt(), anyInt(), anyInt()); - doAnswer(invocation -> { - int localX = invocation.getArgument(0); - int localY = invocation.getArgument(1); - int localZ = invocation.getArgument(2); - MatterCavern value = invocation.getArgument(3); - localCells.put(packLocal(localX, localY, localZ), value); - int worldY = (sectionIndex << 4) + localY; - String cellKey = cellKey(localX, worldY, localZ); - carvedCells.add(cellKey); - carvedLiquids.put(cellKey, value.getLiquid()); - return null; - }).when(slice).set(anyInt(), anyInt(), anyInt(), any(MatterCavern.class)); - return matter; } @@ -1649,10 +1629,6 @@ public class IrisCaveCarver3DNearParityTest { return (value * 2D) - 1D; } - private int packLocal(int x, int y, int z) { - return (x << 8) | (y << 4) | z; - } - private String cellKey(int x, int y, int z) { return x + ":" + y + ":" + z; } @@ -1752,6 +1728,25 @@ public class IrisCaveCarver3DNearParityTest { } } + private final class CapturingCavernMatter extends CavernMatter { + private final int sectionIndex; + private final WriterCapture capture; + + private CapturingCavernMatter(int sectionIndex, WriterCapture capture) { + super(16, 16, 16); + this.sectionIndex = sectionIndex; + this.capture = capture; + } + + @Override + public void set(int x, int y, int z, MatterCavern value) { + super.set(x, y, z, value); + String cell = cellKey(x, (sectionIndex << 4) + y, z); + capture.carvedCells.add(cell); + capture.carvedLiquids.put(cell, value.getLiquid()); + } + } + private static final class WriterCapture { private final MantleWriter writer; private final Set carvedCells; diff --git a/core/src/test/java/art/arcane/iris/generation/runtime/EngineBiomeColumnTest.java b/core/src/test/java/art/arcane/iris/generation/runtime/EngineBiomeColumnTest.java index c0691a103..1bf48551d 100644 --- a/core/src/test/java/art/arcane/iris/generation/runtime/EngineBiomeColumnTest.java +++ b/core/src/test/java/art/arcane/iris/generation/runtime/EngineBiomeColumnTest.java @@ -15,39 +15,43 @@ import art.arcane.volmlib.util.collection.KList; 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.MantleDataAdapter; +import art.arcane.volmlib.util.math.Position2; import art.arcane.volmlib.util.matter.Matter; import art.arcane.volmlib.util.matter.MatterCavern; import art.arcane.volmlib.util.stream.ProceduralStream; +import art.arcane.volmlib.util.stream.interpolation.Interpolated; import org.junit.BeforeClass; import org.junit.Test; import org.mockito.MockedStatic; import java.util.List; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Proxy; import java.util.Random; import static org.junit.Assert.assertSame; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyDouble; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.CALLS_REAL_METHODS; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.when; +import static org.mockito.Mockito.withSettings; public class EngineBiomeColumnTest { private static final int SAMPLES = 96; + private static MantleDataAdapter chunkAdapter; + private static final EnginePlatformHooks PLATFORM_HOOKS = new EnginePlatformHooks() {}; @BeforeClass public static void initializeMantleBlockState() throws Exception { - NativeBlockState air = mock(NativeBlockState.class); + NativeBlockState air = mock(NativeBlockState.class, withSettings().stubOnly()); try (MockedStatic blocks = mockStatic(B.class)) { blocks.when(() -> B.getState("AIR")).thenReturn(air); Class.forName(EngineMantle.class.getName()); + chunkAdapter = IrisEngineMantle.createRuntimeDataAdapter(null); } } @@ -79,20 +83,18 @@ public class EngineBiomeColumnTest { IrisBiome custom = biome("iris:custom_cave", 0); IrisBiome configured = biome("iris:configured_cave", 0); - ProceduralStream surfaceStream = mock(ProceduralStream.class); - when(surfaceStream.get(anyDouble(), anyDouble())).thenAnswer(call -> - surfaces[pick(salt, call.getArgument(0), call.getArgument(1), 1, surfaces.length)]); - ProceduralStream caveStream = mock(ProceduralStream.class); - when(caveStream.get(anyDouble(), anyDouble())).thenAnswer(call -> - caves[pick(salt, call.getArgument(0), call.getArgument(1), 2, caves.length)]); - ProceduralStream regionStream = mock(ProceduralStream.class); - when(regionStream.get(anyDouble(), anyDouble())).thenAnswer(call -> - regions[pick(salt, call.getArgument(0), call.getArgument(1), 3, regions.length)]); - ProceduralStream heightStream = mock(ProceduralStream.class); - when(heightStream.get(anyDouble(), anyDouble())).thenAnswer(call -> - (double) (40 + pick(salt, call.getArgument(0), call.getArgument(1), 4, 300))); + Interpolated biomeInterpolation = Interpolated.of(biome -> 0D, value -> null); + Interpolated regionInterpolation = Interpolated.of(region -> 0D, value -> null); + ProceduralStream surfaceStream = ProceduralStream.of((x, z) -> + surfaces[pick(salt, x, z, 1, surfaces.length)], biomeInterpolation); + ProceduralStream caveStream = ProceduralStream.of((x, z) -> + caves[pick(salt, x, z, 2, caves.length)], biomeInterpolation); + ProceduralStream regionStream = ProceduralStream.of((x, z) -> + regions[pick(salt, x, z, 3, regions.length)], regionInterpolation); + ProceduralStream heightStream = ProceduralStream.of((x, z) -> + (double) (40 + pick(salt, x, z, 4, 300)), Interpolated.DOUBLE); - IrisComplex complex = mock(IrisComplex.class); + IrisComplex complex = mock(IrisComplex.class, withSettings().stubOnly()); when(complex.getTrueBiomeStream()).thenReturn(surfaceStream); when(complex.getCaveBiomeStream()).thenReturn(caveStream); when(complex.getRegionStream()).thenReturn(regionStream); @@ -100,27 +102,23 @@ public class EngineBiomeColumnTest { when(complex.isTerrain3DSurface(anyInt(), anyInt(), anyInt())).thenAnswer(call -> pick(salt, (int) call.getArgument(0) * 31 + (int) call.getArgument(1), (int) call.getArgument(2), 5, 9) == 0); - Mantle mantle = mock(Mantle.class); + Mantle mantle = mock(Mantle.class, withSettings().stubOnly()); when(mantle.getWorldHeight()).thenReturn(384); when(mantle.getLoadedRegions()).thenReturn(new KMap<>()); when(mantle.hasTectonicPlate(anyInt(), anyInt())).thenReturn(true); when(mantle.get(anyInt(), anyInt(), anyInt(), eq(MatterCavern.class))).thenAnswer(call -> cavern(salt, call.getArgument(0), call.getArgument(1), call.getArgument(2))); - when(mantle.useChunk(anyInt(), anyInt())).thenAnswer(call -> { - int chunkX = call.getArgument(0); - int chunkZ = call.getArgument(1); - MantleChunk chunk = mock(MantleChunk.class); - when(chunk.get(anyInt(), anyInt(), anyInt(), eq(MatterCavern.class))).thenAnswer(read -> cavern(salt, - chunkX * 16 + (int) read.getArgument(0), read.getArgument(1), chunkZ * 16 + (int) read.getArgument(2))); - return chunk; - }); - EngineMantle engineMantle = mock(EngineMantle.class); - when(engineMantle.getMantle()).thenReturn(mantle); - doAnswer(call -> 20 + pick(salt, (int) call.getArgument(0), (int) call.getArgument(1), 7, 330)) - .when(engineMantle).getHighest(anyInt(), anyInt(), any(IrisData.class), anyBoolean()); + when(mantle.useChunk(anyInt(), anyInt())).thenAnswer(call -> + new CoordinateChunk(salt, new Position2(call.getArgument(0), call.getArgument(1))).use()); + EngineMantle engineMantle = (EngineMantle) Proxy.newProxyInstance(EngineMantle.class.getClassLoader(), + new Class[]{EngineMantle.class}, (proxy, method, arguments) -> switch (method.getName()) { + case "getMantle" -> mantle; + case "getHighest" -> 20 + pick(salt, (int) arguments[0], (int) arguments[1], 7, 330); + default -> throw new UnsupportedOperationException(method.getName()); + }); - IrisData data = mock(IrisData.class); - ResourceLoader loader = mock(ResourceLoader.class); + IrisData data = mock(IrisData.class, withSettings().stubOnly()); + ResourceLoader loader = mock(ResourceLoader.class, withSettings().stubOnly()); when(data.getBiomeLoader()).thenReturn(loader); when(loader.load(anyString())).thenReturn(null); when(loader.load("iris:custom_cave")).thenReturn(custom); @@ -136,15 +134,35 @@ public class EngineBiomeColumnTest { dimension.setCarving(new KList<>(List.of(root))); } - EnginePlatformHooks hooks = mock(EnginePlatformHooks.class); - Engine engine = mock(Engine.class, CALLS_REAL_METHODS); - doReturn(complex).when(engine).getComplex(); - doReturn(engineMantle).when(engine).getMantle(); - doReturn(hooks).when(engine).getPlatformHooks(); - doReturn(data).when(engine).getData(); - doReturn(dimension).when(engine).getDimension(); - doReturn(IrisWorld.builder().minHeight(-64).maxHeight(320).build()).when(engine).getWorld(); - return engine; + IrisWorld world = IrisWorld.builder().minHeight(-64).maxHeight(320).build(); + return (Engine) Proxy.newProxyInstance(Engine.class.getClassLoader(), new Class[]{Engine.class}, + (proxy, method, arguments) -> switch (method.getName()) { + case "getComplex" -> complex; + case "getMantle" -> engineMantle; + case "getPlatformHooks" -> PLATFORM_HOOKS; + case "getData" -> data; + case "getDimension" -> dimension; + case "getWorld" -> world; + default -> InvocationHandler.invokeDefault(proxy, method, arguments); + }); + } + + private static final class CoordinateChunk extends MantleChunk { + private final long salt; + private final Position2 coordinates; + + private CoordinateChunk(long salt, Position2 coordinates) { + super(24, coordinates.getX(), coordinates.getZ(), chunkAdapter, null); + this.salt = salt; + this.coordinates = coordinates; + } + + @Override + public T get(int x, int y, int z, Class type) { + return type == MatterCavern.class + ? type.cast(cavern(salt, coordinates.getX() * 16 + x, y, coordinates.getZ() * 16 + z)) + : null; + } } private static MatterCavern cavern(long salt, int x, int y, int z) { diff --git a/core/src/test/java/art/arcane/iris/generation/stage/IrisDepositModifierOrderingTest.java b/core/src/test/java/art/arcane/iris/generation/stage/IrisDepositModifierOrderingTest.java index a72f64587..64c6314b0 100644 --- a/core/src/test/java/art/arcane/iris/generation/stage/IrisDepositModifierOrderingTest.java +++ b/core/src/test/java/art/arcane/iris/generation/stage/IrisDepositModifierOrderingTest.java @@ -3,6 +3,8 @@ package art.arcane.iris.generation.stage; import art.arcane.iris.testsupport.KeyedBlockState; import art.arcane.iris.generation.runtime.Engine; +import art.arcane.iris.generation.biome.IrisBiome; +import art.arcane.iris.generation.terrain.IrisRegion; import art.arcane.iris.generation.decoration.IrisDepositGenerator; import art.arcane.iris.generation.decoration.IrisDepositHeightDistribution; import art.arcane.iris.generation.decoration.IrisDepositPlacementScope; @@ -17,6 +19,9 @@ import art.arcane.iris.generation.geometry.IrisBlockVector; import art.arcane.iris.generation.concurrent.BurstExecutor; import art.arcane.iris.generation.concurrent.MultiBurst; import art.arcane.iris.generation.context.ChunkContext; +import art.arcane.iris.generation.context.ChunkedDataCache; +import art.arcane.volmlib.util.stream.ProceduralStream; +import art.arcane.volmlib.util.stream.interpolation.Interpolated; import art.arcane.volmlib.util.hunk.Hunk; import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.mantle.runtime.MantleChunk; @@ -50,6 +55,7 @@ import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.mockito.Mockito.withSettings; public class IrisDepositModifierOrderingTest { @ClassRule @@ -58,10 +64,10 @@ public class IrisDepositModifierOrderingTest { @Before public void bindPlatform() { IrisPlatforms.unbind(); - PlatformRegistries registries = mock(PlatformRegistries.class); + PlatformRegistries registries = mock(PlatformRegistries.class, withSettings().stubOnly()); NativeBlockState stone = state("stone"); when(registries.block(anyString())).thenReturn(stone); - IrisPlatform platform = mock(IrisPlatform.class); + IrisPlatform platform = mock(IrisPlatform.class, withSettings().stubOnly()); when(platform.registries()).thenReturn(registries); IrisPlatforms.bind(platform); } @@ -274,8 +280,8 @@ public class IrisDepositModifierOrderingTest { private final List samples = Collections.synchronizedList(new ArrayList<>()); private final CountDownLatch biomeReady = new CountDownLatch(1); private final CountDownLatch regionReady = new CountDownLatch(1); - private final Engine engine = mock(Engine.class, RETURNS_DEEP_STUBS); - private final ChunkContext context = mock(ChunkContext.class, RETURNS_DEEP_STUBS); + private final Engine engine = mock(Engine.class, withSettings().stubOnly().defaultAnswer(RETURNS_DEEP_STUBS)); + private final ChunkContext context = mock(ChunkContext.class, withSettings().stubOnly().defaultAnswer(RETURNS_DEEP_STUBS)); private final NativeBlockState stone = state("stone"); private final NativeBlockState granite = state("granite"); private final NativeBlockState diorite = state("diorite"); @@ -289,15 +295,23 @@ public class IrisDepositModifierOrderingTest { private Fixture(boolean parallel, int attempts) { this.parallel = parallel; this.attempts = attempts; + IrisBiome biome = mock(IrisBiome.class, withSettings().stubOnly()); + IrisRegion region = mock(IrisRegion.class, withSettings().stubOnly()); + ChunkedDataCache biomes = new ChunkedDataCache<>( + ProceduralStream.of((x, z) -> biome, Interpolated.of(value -> 0D, value -> null)), 0, 0); + ChunkedDataCache regions = new ChunkedDataCache<>( + ProceduralStream.of((x, z) -> region, Interpolated.of(value -> 0D, value -> null)), 0, 0); + when(context.getBiome()).thenReturn(biomes); + when(context.getRegion()).thenReturn(regions); when(engine.getHeight()).thenReturn(16); when(engine.getCaveBiome(anyInt(), anyInt(), anyInt(), any())).thenReturn(null); when(engine.getDimension().getDepositVariants()).thenReturn(new KList<>()); - when(context.getRegion().get(anyInt(), anyInt()).getDepositVariants()).thenReturn(new KList<>()); + when(region.getDepositVariants()).thenReturn(new KList<>()); when(context.getGenerationSessionId()).thenReturn(11L); chunk = mock(MantleChunk.class); Mantle mantle = engine.getMantle().getMantle(); doReturn(chunk).when(mantle).useChunk(0, 0); - MultiBurst pool = mock(MultiBurst.class); + MultiBurst pool = mock(MultiBurst.class, withSettings().stubOnly()); BurstExecutor burst = new BurstExecutor(executor, 3); burst.setMulticore(parallel); when(engine.burst()).thenReturn(pool); @@ -306,8 +320,8 @@ public class IrisDepositModifierOrderingTest { IrisDepositGenerator regionDeposit = generator("region", granite, diorite); IrisDepositGenerator biomeDeposit = generator("biome", diorite, diamond); when(engine.getDimension().getDeposits()).thenReturn(new KList<>(dimensionDeposit)); - when(context.getRegion().get(7, 7).getDeposits()).thenReturn(new KList<>(regionDeposit)); - when(context.getBiome().get(7, 7).getDeposits()).thenReturn(new KList<>(biomeDeposit)); + when(region.getDeposits()).thenReturn(new KList<>(regionDeposit)); + when(biome.getDeposits()).thenReturn(new KList<>(biomeDeposit)); for (int x = 0; x < 16; x++) { for (int z = 0; z < 16; z++) { output.set(x, 4, z, stone); @@ -316,7 +330,7 @@ public class IrisDepositModifierOrderingTest { } private IrisDepositGenerator generator(String name, NativeBlockState requiredHost, NativeBlockState replacement) { - IrisDepositGenerator generator = mock(IrisDepositGenerator.class); + IrisDepositGenerator generator = mock(IrisDepositGenerator.class, withSettings().stubOnly()); when(generator.getSpawnChance()).thenReturn(1D); when(generator.getPerClumpSpawnChance()).thenReturn(1D); when(generator.getMinPerChunk()).thenReturn(attempts); @@ -355,7 +369,7 @@ public class IrisDepositModifierOrderingTest { } private void generate() { - try (MockedStatic blocks = mockStatic(B.class, CALLS_REAL_METHODS)) { + try (MockedStatic blocks = mockStatic(B.class, withSettings().stubOnly().defaultAnswer(CALLS_REAL_METHODS))) { blocks.when(() -> B.toDeepSlateOre(any(), any())).thenAnswer(invocation -> invocation.getArgument(1)); new IrisDepositModifier(engine).generateDeposits(output, 0, 0, parallel, context); } diff --git a/core/src/test/java/art/arcane/iris/generation/stage/IrisDepositModifierParityTest.java b/core/src/test/java/art/arcane/iris/generation/stage/IrisDepositModifierParityTest.java index 94d872dfa..e3d4ba776 100644 --- a/core/src/test/java/art/arcane/iris/generation/stage/IrisDepositModifierParityTest.java +++ b/core/src/test/java/art/arcane/iris/generation/stage/IrisDepositModifierParityTest.java @@ -21,6 +21,9 @@ import art.arcane.iris.testsupport.PlatformLeakGuard; import art.arcane.iris.generation.block.B; import art.arcane.iris.generation.concurrent.MultiBurst; import art.arcane.iris.generation.context.ChunkContext; +import art.arcane.iris.generation.context.ChunkedDataCache; +import art.arcane.volmlib.util.stream.ProceduralStream; +import art.arcane.volmlib.util.stream.interpolation.Interpolated; import art.arcane.volmlib.util.hunk.Hunk; import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.mantle.runtime.Mantle; @@ -53,6 +56,7 @@ import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.when; +import static org.mockito.Mockito.withSettings; public class IrisDepositModifierParityTest { @ClassRule @@ -63,9 +67,9 @@ public class IrisDepositModifierParityTest { @Before public void bindPlatform() { IrisPlatforms.unbind(); - PlatformRegistries registries = mock(PlatformRegistries.class); + PlatformRegistries registries = mock(PlatformRegistries.class, withSettings().stubOnly()); when(registries.block(anyString())).thenAnswer(invocation -> state(invocation.getArgument(0))); - IrisPlatform platform = mock(IrisPlatform.class); + IrisPlatform platform = mock(IrisPlatform.class, withSettings().stubOnly()); when(platform.registries()).thenReturn(registries); IrisPlatforms.bind(platform); } @@ -125,8 +129,8 @@ public class IrisDepositModifierParityTest { @SuppressWarnings("unchecked") private long[] generateConfigured(long seed, boolean multicore, boolean caveVariant, boolean enumerablePack, AtomicInteger caveBiomeLookups, AtomicInteger unmatchableClumps) { - Engine engine = mock(Engine.class, RETURNS_DEEP_STUBS); - ChunkContext context = mock(ChunkContext.class, RETURNS_DEEP_STUBS); + Engine engine = mock(Engine.class, withSettings().stubOnly().defaultAnswer(RETURNS_DEEP_STUBS)); + ChunkContext context = mock(ChunkContext.class, withSettings().stubOnly().defaultAnswer(RETURNS_DEEP_STUBS)); IrisDimension dimension = new IrisDimension(); IrisRegion region = new IrisRegion(); IrisBiome surface = new IrisBiome(); @@ -170,9 +174,9 @@ public class IrisDepositModifierParityTest { when(engine.getSeedManager().getDeposit()).thenReturn(seed); when(engine.getDimension()).thenReturn(dimension); if (enumerablePack) { - IrisData data = mock(IrisData.class); - ResourceLoader biomes = mock(ResourceLoader.class); - ResourceLoader regions = mock(ResourceLoader.class); + IrisData data = mock(IrisData.class, withSettings().stubOnly()); + ResourceLoader biomes = mock(ResourceLoader.class, withSettings().stubOnly()); + ResourceLoader regions = mock(ResourceLoader.class, withSettings().stubOnly()); when(data.getBiomeLoader()).thenReturn(biomes); when(data.getRegionLoader()).thenReturn(regions); when(biomes.getPossibleKeys()).thenReturn(new String[]{"allowed", "denied", "surface"}); @@ -192,9 +196,13 @@ public class IrisDepositModifierParityTest { return ((Math.floorDiv(x, 8) + Math.floorDiv(z, 8) + Math.floorDiv(y, 8)) & 1) == 0 ? cave : denied; }); when(context.getRoundedHeight(anyInt(), anyInt())).thenReturn(40); - when(context.getBiome().get(anyInt(), anyInt())).thenReturn(surface); - when(context.getRegion().get(anyInt(), anyInt())).thenReturn(region); - MantleChunk chunk = mock(MantleChunk.class); + ChunkedDataCache biomes = new ChunkedDataCache<>( + ProceduralStream.of((x, z) -> surface, Interpolated.of(value -> 0D, value -> null)), 0, 0); + ChunkedDataCache regions = new ChunkedDataCache<>( + ProceduralStream.of((x, z) -> region, Interpolated.of(value -> 0D, value -> null)), 0, 0); + when(context.getBiome()).thenReturn(biomes); + when(context.getRegion()).thenReturn(regions); + MantleChunk chunk = mock(MantleChunk.class, withSettings().stubOnly()); Mantle mantle = engine.getMantle().getMantle(); doReturn(chunk).when(mantle).useChunk(-3, 5); MatterCavern cavern = new MatterCavern(true, "allowed", (byte) 0); @@ -215,7 +223,7 @@ public class IrisDepositModifierParityTest { } MultiBurst burst = new MultiBurst("Deposit parity", () -> 4); when(engine.burst()).thenReturn(burst); - try (MockedStatic blocks = mockStatic(B.class, CALLS_REAL_METHODS)) { + try (MockedStatic blocks = mockStatic(B.class, withSettings().stubOnly().defaultAnswer(CALLS_REAL_METHODS))) { blocks.when(() -> B.getStateOrNull(anyString(), anyBoolean())) .thenAnswer(invocation -> state(invocation.getArgument(0))); blocks.when(() -> B.toDeepSlateOre(any(), any())).thenAnswer(invocation -> { diff --git a/core/src/test/java/art/arcane/iris/generation/stage/IrisPerfectionModifierColumnTest.java b/core/src/test/java/art/arcane/iris/generation/stage/IrisPerfectionModifierColumnTest.java index e5f9618f9..ea1a7ec84 100644 --- a/core/src/test/java/art/arcane/iris/generation/stage/IrisPerfectionModifierColumnTest.java +++ b/core/src/test/java/art/arcane/iris/generation/stage/IrisPerfectionModifierColumnTest.java @@ -30,7 +30,6 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.RETURNS_DEEP_STUBS; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -245,27 +244,150 @@ public class IrisPerfectionModifierColumnTest { if (existing != null) { return existing; } - NativeBlockState block = mock(NativeBlockState.class, withSettings().stubOnly()); + NativeBlockState block = new TestBlockState(this, key); states.put(key, block); - when(block.key()).thenReturn(key); - when(block.isAir()).thenReturn(key.equals("minecraft:air")); - when(block.isFluid()).thenReturn(key.equals("minecraft:water")); - when(block.isWater()).thenReturn(key.equals("minecraft:water")); - when(block.isWaterLogged()).thenReturn(key.contains("waterlogged=true")); - when(block.isDecorant()).thenReturn(key.equals("minecraft:poppy") || key.startsWith("minecraft:tall_grass")); - when(block.canPlaceOnto(any())).thenAnswer(call -> "minecraft:tall_grass[half=lower]".equals(call.getArgument(0).key())); - BlockData handle = mock(BlockData.class, withSettings().stubOnly()); - when(block.nativeHandle()).thenReturn(handle); - when(handle.isFaceSturdy(any(), any())).thenReturn(key.equals("minecraft:stone")); - when(block.withProperty(anyString(), anyString())).thenAnswer(call -> { - String property = call.getArgument(0); - String value = call.getArgument(1); - return state(key.replaceAll(property + "=[^,\\]]+", property + "=" + value)); - }); return block; } } + private static final class TestBlockState implements NativeBlockState { + private final Fixture fixture; + private final String key; + private final BlockData handle; + + private TestBlockState(Fixture fixture, String key) { + this.fixture = fixture; + this.key = key; + handle = mock(BlockData.class, withSettings().stubOnly()); + when(handle.isFaceSturdy(any(), any())).thenReturn(key.equals("minecraft:stone")); + } + + @Override + public String key() { + return key; + } + + @Override + public String namespace() { + return null; + } + + @Override + public boolean isAir() { + return key.equals("minecraft:air"); + } + + @Override + public boolean isFluid() { + return key.equals("minecraft:water"); + } + + @Override + public boolean isWater() { + return key.equals("minecraft:water"); + } + + @Override + public boolean isWaterLogged() { + return key.contains("waterlogged=true"); + } + + @Override + public boolean isDecorant() { + return key.equals("minecraft:poppy") || key.startsWith("minecraft:tall_grass"); + } + + @Override + public boolean canPlaceOnto(NativeBlockState onto) { + return "minecraft:tall_grass[half=lower]".equals(onto.key()); + } + + @Override + public Object nativeHandle() { + return handle; + } + + @Override + public NativeBlockState withProperty(String property, String value) { + return fixture.state(key.replaceAll(property + "=[^,\\]]+", property + "=" + value)); + } + + @Override + public boolean isSolid() { + return false; + } + + @Override + public boolean isOccluding() { + return false; + } + + @Override + public boolean isCustom() { + return false; + } + + @Override + public boolean isLit() { + return false; + } + + @Override + public boolean isUpdatable() { + return false; + } + + @Override + public boolean isFoliage() { + return false; + } + + @Override + public boolean isTreeBlock() { + return false; + } + + @Override + public boolean isFoliagePlantable() { + return false; + } + + @Override + public boolean isStorage() { + return false; + } + + @Override + public boolean isStorageChest() { + return false; + } + + @Override + public boolean isOre() { + return false; + } + + @Override + public boolean isDeepSlate() { + return false; + } + + @Override + public boolean isVineBlock() { + return false; + } + + @Override + public boolean hasTileEntity() { + return false; + } + + @Override + public boolean matches(NativeBlockState state) { + return false; + } + } + private static final class CountingHunk extends ArrayHunk { private int cleanColumnReads; diff --git a/core/src/test/java/art/arcane/iris/generation/terrain/IrisDimensionCarvingResolverParityTest.java b/core/src/test/java/art/arcane/iris/generation/terrain/IrisDimensionCarvingResolverParityTest.java index ecaf8687a..2dbd4a142 100644 --- a/core/src/test/java/art/arcane/iris/generation/terrain/IrisDimensionCarvingResolverParityTest.java +++ b/core/src/test/java/art/arcane/iris/generation/terrain/IrisDimensionCarvingResolverParityTest.java @@ -18,6 +18,7 @@ import art.arcane.volmlib.util.collection.KList; import org.junit.BeforeClass; import org.junit.Test; import org.mockito.invocation.InvocationOnMock; +import org.mockito.MockMakers; import java.lang.ref.WeakReference; import java.lang.reflect.Field; @@ -47,6 +48,7 @@ import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.withSettings; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; @@ -348,7 +350,7 @@ public class IrisDimensionCarvingResolverParityTest { Fixture replacement = createMixedDepthFixture(); AtomicReference dimension = new AtomicReference<>(first.engine.getDimension()); AtomicReference data = new AtomicReference<>(first.engine.getData()); - Engine engine = mock(Engine.class, CALLS_REAL_METHODS); + Engine engine = mock(Engine.class, withSettings().defaultAnswer(CALLS_REAL_METHODS).mockMaker(MockMakers.PROXY)); doAnswer((InvocationOnMock invocation) -> dimension.get()).when(engine).getDimension(); doAnswer((InvocationOnMock invocation) -> data.get()).when(engine).getData(); @@ -601,7 +603,7 @@ public class IrisDimensionCarvingResolverParityTest { doReturn(carvingEntries).when(dimension).getCarving(); doReturn(index).when(dimension).getCarvingEntryIndex(); - Engine engine = mock(Engine.class, CALLS_REAL_METHODS); + Engine engine = mock(Engine.class, withSettings().defaultAnswer(CALLS_REAL_METHODS).mockMaker(MockMakers.PROXY)); doReturn(dimension).when(engine).getDimension(); doReturn(data).when(engine).getData(); doReturn(new SeedManager(913_531_771L)).when(engine).getSeedManager(); @@ -689,7 +691,7 @@ public class IrisDimensionCarvingResolverParityTest { doReturn(carvingEntries).when(dimension).getCarving(); doReturn(index).when(dimension).getCarvingEntryIndex(); - Engine engine = mock(Engine.class, CALLS_REAL_METHODS); + Engine engine = mock(Engine.class, withSettings().defaultAnswer(CALLS_REAL_METHODS).mockMaker(MockMakers.PROXY)); doReturn(dimension).when(engine).getDimension(); doReturn(data).when(engine).getData(); doReturn(new SeedManager(4_627_991_643L)).when(engine).getSeedManager(); diff --git a/core/src/test/java/art/arcane/iris/studio/view/PregeneratorJobTerminalProgressTest.java b/core/src/test/java/art/arcane/iris/studio/view/PregeneratorJobTerminalProgressTest.java index 159e06550..b980d6920 100644 --- a/core/src/test/java/art/arcane/iris/studio/view/PregeneratorJobTerminalProgressTest.java +++ b/core/src/test/java/art/arcane/iris/studio/view/PregeneratorJobTerminalProgressTest.java @@ -2,6 +2,7 @@ package art.arcane.iris.studio.view; import art.arcane.iris.configuration.IrisSettings; import art.arcane.iris.world.pregen.PregenApiPhase; +import art.arcane.iris.world.pregen.MantleHeapPressure; import art.arcane.iris.world.pregen.PregenApiSink; import art.arcane.iris.world.pregen.PregenListener; import art.arcane.iris.world.pregen.PregenTask; @@ -9,11 +10,13 @@ import art.arcane.iris.world.pregen.PregeneratorMethod; import art.arcane.iris.spi.IrisServices; import art.arcane.volmlib.util.math.Position2; import org.junit.Test; +import org.mockito.MockedStatic; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicReference; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -23,6 +26,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.when; public class PregeneratorJobTerminalProgressTest { @@ -89,6 +93,7 @@ public class PregeneratorJobTerminalProgressTest { PregeneratorMethod method = mock(PregeneratorMethod.class); when(method.getMethod(anyInt(), anyInt())).thenReturn("terminal-progress-test"); ArrayList pending = new ArrayList<>(); + AtomicReference> heapPressure = new AtomicReference<>(); doAnswer(invocation -> { pending.add(invocation.getArgument(2)); if (completion.cancel()) { @@ -97,12 +102,19 @@ public class PregeneratorJobTerminalProgressTest { return null; }).when(method).generateChunk(anyInt(), anyInt(), any(PregenListener.class)); doAnswer(invocation -> { - for (int index = 0; index < pending.size(); index++) { - PregenListener listener = pending.get(index); - if (completion.failLast() && index == pending.size() - 1) { - listener.onChunkFailed(0, 0); - } else { - listener.onChunkGenerated(0, 0, completion.cached()); + try { + for (int index = 0; index < pending.size(); index++) { + PregenListener listener = pending.get(index); + if (completion.failLast() && index == pending.size() - 1) { + listener.onChunkFailed(0, 0); + } else { + listener.onChunkGenerated(0, 0, completion.cached()); + } + } + } finally { + MockedStatic pressure = heapPressure.getAndSet(null); + if (pressure != null) { + pressure.close(); } } return null; @@ -112,7 +124,8 @@ public class PregeneratorJobTerminalProgressTest { Thread worker = null; try { assertNull(PregeneratorJob.getInstance()); - PregeneratorJob job = new PregeneratorJob(new PregeneratorJob.Configuration(task, method, null, () -> {})); + PregeneratorJob job = new PregeneratorJob(new PregeneratorJob.Configuration(task, method, null, + () -> heapPressure.set(mockStatic(MantleHeapPressure.class)))); Field workerField = PregeneratorJob.class.getDeclaredField("worker"); workerField.setAccessible(true); worker = (Thread) workerField.get(job); diff --git a/core/src/test/java/art/arcane/iris/world/history/SavedBiomeCaptureTest.java b/core/src/test/java/art/arcane/iris/world/history/SavedBiomeCaptureTest.java index c655da1c4..b652f046c 100644 --- a/core/src/test/java/art/arcane/iris/world/history/SavedBiomeCaptureTest.java +++ b/core/src/test/java/art/arcane/iris/world/history/SavedBiomeCaptureTest.java @@ -1,6 +1,8 @@ package art.arcane.iris.world.history; import art.arcane.iris.generation.runtime.Engine; +import art.arcane.iris.generation.runtime.IrisComplex; +import art.arcane.volmlib.util.stream.ProceduralStream; import art.arcane.iris.generation.runtime.DimensionStackContext; import art.arcane.iris.generation.runtime.DimensionStackLayout; import art.arcane.iris.generation.subterrain.SubterrainCell; @@ -87,7 +89,7 @@ public class SavedBiomeCaptureTest { @Test public void retainsFloatingChildIdentityWhenItSharesTheHostDerivative() throws Exception { - Engine engine = mock(Engine.class, RETURNS_DEEP_STUBS); + Engine engine = captureEngine(); when(engine.getComplex().getSubterrainPlanner()).thenReturn(null); GenerationHistory.GenerationStage stage = mock(GenerationHistory.GenerationStage.class); SavedBiomeRuntime historical = mock(SavedBiomeRuntime.class); @@ -129,7 +131,7 @@ public class SavedBiomeCaptureTest { @Test public void recordsExactSurfaceColumnsAndIndependentCaveAndVerticalIdentities() throws Exception { - Engine engine = mock(Engine.class, RETURNS_DEEP_STUBS); + Engine engine = captureEngine(); when(engine.getComplex().getSubterrainPlanner()).thenReturn(null); GenerationHistory.GenerationStage stage = mock(GenerationHistory.GenerationStage.class); SavedBiomeRuntime historical = mock(SavedBiomeRuntime.class); @@ -170,15 +172,15 @@ public class SavedBiomeCaptureTest { @Test public void preservesHostFeatureBiomeAndRegionAgainstStackAndFloatingOverlay() throws Exception { - Engine engine = mock(Engine.class, RETURNS_DEEP_STUBS); + Engine engine = captureEngine(); when(engine.getComplex().getSubterrainPlanner()).thenReturn(null); IrisBiome upper = biome("upper-biome"); IrisBiome authored = biome("authored-biome"); IrisRegion upperRegion = region("upper-region"); IrisRegion hostRegion = region("host-region"); - DimensionStackContext stack = mock(DimensionStackContext.class); - DimensionStackLayout layout = mock(DimensionStackLayout.class); - DimensionStackLayout.Layer layer = mock(DimensionStackLayout.Layer.class); + DimensionStackContext stack = mock(DimensionStackContext.class, withSettings().stubOnly()); + DimensionStackLayout layout = mock(DimensionStackLayout.class, withSettings().stubOnly()); + DimensionStackLayout.Layer layer = mock(DimensionStackLayout.Layer.class, withSettings().stubOnly()); when(engine.getDimensionStackContext()).thenReturn(stack); when(stack.getLayout(anyInt(), anyInt())).thenReturn(layout); when(layout.layerAt(anyInt())).thenReturn(layer); @@ -220,15 +222,14 @@ public class SavedBiomeCaptureTest { } int chunkX = Math.floorDiv(edgeX, 16); int chunkZ = Math.floorDiv(anchor.z(), 16); - Engine engine = mock(Engine.class, withSettings().defaultAnswer(RETURNS_DEEP_STUBS) - .mockMaker(MockMakers.SUBCLASS)); + Engine engine = captureEngine(); IrisBiome fallback = biome("base-biome"); IrisBiome authored = biome("authored-biome"); IrisRegion host = region("host-region"); IrisRegion stacked = region("stacked-region"); - DimensionStackContext stack = mock(DimensionStackContext.class); - DimensionStackLayout layout = mock(DimensionStackLayout.class); - DimensionStackLayout.Layer layer = mock(DimensionStackLayout.Layer.class); + DimensionStackContext stack = mock(DimensionStackContext.class, withSettings().stubOnly()); + DimensionStackLayout layout = mock(DimensionStackLayout.class, withSettings().stubOnly()); + DimensionStackLayout.Layer layer = mock(DimensionStackLayout.Layer.class, withSettings().stubOnly()); when(engine.getDimensionStackContext()).thenReturn(stack); when(stack.getLayout(anyInt(), anyInt())).thenReturn(layout); when(layout.layerAt(anyInt())).thenReturn(layer); @@ -272,8 +273,21 @@ public class SavedBiomeCaptureTest { assertTrue(outsideOccupiedVolume); } + @SuppressWarnings("unchecked") + private static Engine captureEngine() { + Engine engine = mock(Engine.class, withSettings().stubOnly().mockMaker(MockMakers.PROXY)); + IrisComplex complex = mock(IrisComplex.class, withSettings().stubOnly().defaultAnswer(RETURNS_DEEP_STUBS)); + ProceduralStream regions = mock(ProceduralStream.class, withSettings().stubOnly()); + ProceduralStream caves = mock(ProceduralStream.class, withSettings().stubOnly()); + when(engine.getComplex()).thenReturn(complex); + when(engine.getSubterrainCell(anyInt(), anyInt(), anyInt())).thenReturn(SubterrainCell.OUTSIDE); + when(complex.getRegionStream()).thenReturn(regions); + when(complex.getCaveBiomeStream()).thenReturn(caves); + return engine; + } + private static Engine caveCaptureEngine(IrisBiome surface, IrisBiome cave) { - Engine engine = mock(Engine.class, RETURNS_DEEP_STUBS); + Engine engine = captureEngine(); when(engine.getComplex().getSubterrainPlanner()).thenReturn(null); IrisRegion region = region("flat-region"); when(engine.getMinHeight()).thenReturn(-64); diff --git a/core/src/test/java/art/arcane/iris/world/history/SavedBiomeRuntimeTest.java b/core/src/test/java/art/arcane/iris/world/history/SavedBiomeRuntimeTest.java index 71ea2242e..c367ca599 100644 --- a/core/src/test/java/art/arcane/iris/world/history/SavedBiomeRuntimeTest.java +++ b/core/src/test/java/art/arcane/iris/world/history/SavedBiomeRuntimeTest.java @@ -35,6 +35,7 @@ import java.util.concurrent.TimeoutException; import java.util.concurrent.ExecutionException; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.concurrent.locks.LockSupport; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -589,8 +590,14 @@ public class SavedBiomeRuntimeTest { } private static void awaitIdle(SavedBiomeRuntime runtime) { - Await.reached("the runtime to drain its pending queries", Duration.ofSeconds(5L), - () -> runtime.pendingQueryCount() == 0); + long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5L); + while (runtime.pendingQueryCount() != 0 && System.nanoTime() - deadline < 0L) { + LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(1L)); + if (Thread.interrupted()) { + Thread.currentThread().interrupt(); + throw new AssertionError("Interrupted while waiting for the runtime to drain its pending queries"); + } + } assertEquals(0, runtime.pendingQueryCount()); } diff --git a/probe/src/main/java/art/arcane/iris/generation/runtime/ProbeBackgroundTasks.java b/probe/src/main/java/art/arcane/iris/generation/runtime/ProbeBackgroundTasks.java new file mode 100644 index 000000000..192ac74a0 --- /dev/null +++ b/probe/src/main/java/art/arcane/iris/generation/runtime/ProbeBackgroundTasks.java @@ -0,0 +1,17 @@ +package art.arcane.iris.generation.runtime; + +public final class ProbeBackgroundTasks { + private ProbeBackgroundTasks() { + } + + public static void await(Engine engine) { + if (!(engine instanceof IrisEngine iris)) { + throw new IllegalArgumentException("Probe background barrier requires an Iris engine"); + } + await(iris.backgroundTasks); + } + + static void await(EngineBackgroundTasks tasks) { + tasks.drainBackgroundTasks("probe completion").requireComplete("probe completion"); + } +} diff --git a/probe/src/main/java/art/arcane/iris/probe/GenerationOrderProbe.java b/probe/src/main/java/art/arcane/iris/probe/GenerationOrderProbe.java index 5644ea040..e267490c6 100644 --- a/probe/src/main/java/art/arcane/iris/probe/GenerationOrderProbe.java +++ b/probe/src/main/java/art/arcane/iris/probe/GenerationOrderProbe.java @@ -414,7 +414,7 @@ public final class GenerationOrderProbe { ? generateParallel(session.engine(), scheduled, configuration) : generateSequential(session.engine(), scheduled, configuration); long generationNanos = System.nanoTime() - started; - List reports = RealPackProbeSupport.settleAndDrain(); + List reports = RealPackProbeSupport.settleAndDrain(session.engine()); if (!reports.isEmpty()) { RealPackProbeSupport.printReports(LOG_PREFIX, order + " generation reports", reports); throw reportedFailure(order + " generation", reports); diff --git a/probe/src/main/java/art/arcane/iris/probe/HydrologyPackProbe.java b/probe/src/main/java/art/arcane/iris/probe/HydrologyPackProbe.java index 583ab85d0..59be6d6ff 100644 --- a/probe/src/main/java/art/arcane/iris/probe/HydrologyPackProbe.java +++ b/probe/src/main/java/art/arcane/iris/probe/HydrologyPackProbe.java @@ -3156,7 +3156,7 @@ public final class HydrologyPackProbe { } hydrologySeed = engine.getSeedManager().getBodies(); engineReadyNanos = session.readyNanos(); - List reports = RealPackProbeSupport.settleAndDrain(); + List reports = RealPackProbeSupport.settleAndDrain(engine); if (!reports.isEmpty()) { RealPackProbeSupport.printReports(LOG_PREFIX, "seed " + seed + " planning reports", reports); throw reportedFailure("Hydrology coverage scan", reports); @@ -4153,7 +4153,7 @@ public final class HydrologyPackProbe { System.out.println(new GeneratedProcessResult( witness.selector(), verification).machineLine()); } - List reports = RealPackProbeSupport.settleAndDrain(); + List reports = RealPackProbeSupport.settleAndDrain(engine); if (!reports.isEmpty()) { RealPackProbeSupport.printReports(LOG_PREFIX, "generated chunk reports", reports); throw reportedFailure("Hydrology generated chunk process", reports); diff --git a/probe/src/main/java/art/arcane/iris/probe/RealPackProbeSupport.java b/probe/src/main/java/art/arcane/iris/probe/RealPackProbeSupport.java index a7689a363..41a23172d 100644 --- a/probe/src/main/java/art/arcane/iris/probe/RealPackProbeSupport.java +++ b/probe/src/main/java/art/arcane/iris/probe/RealPackProbeSupport.java @@ -1,9 +1,11 @@ package art.arcane.iris.probe; import art.arcane.iris.pack.loading.IrisData; +import art.arcane.iris.generation.concurrent.MultiBurst; import art.arcane.iris.pack.PackValidationResult; import art.arcane.iris.pack.PackValidator; import art.arcane.iris.generation.runtime.IrisEngine; +import art.arcane.iris.generation.runtime.ProbeBackgroundTasks; import art.arcane.iris.generation.runtime.Engine; import art.arcane.iris.generation.runtime.EngineAssignedComponent; import art.arcane.iris.generation.runtime.EngineEffects; @@ -39,6 +41,7 @@ import java.util.EnumSet; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.LinkedHashMap; @@ -46,6 +49,13 @@ import java.util.List; import java.util.Map; import java.util.Objects; import java.util.concurrent.ExecutorService; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.ForkJoinTask; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.locks.LockSupport; import java.util.stream.Stream; final class RealPackProbeSupport { @@ -79,6 +89,7 @@ final class RealPackProbeSupport { validatePack(pack, logPrefix); return new Workspace(workRoot, pack, dimensionKey, logPrefix, false, bindings); } catch (Throwable failure) { + clearRuntime(); Throwable cleanupFailure = deleteWorkRoot(workRoot); if (cleanupFailure != null) { failure.addSuppressed(cleanupFailure); @@ -103,6 +114,7 @@ final class RealPackProbeSupport { configureRuntime(new File(workRoot, "platform-validation"), hydrologyGeneratedVerification, null); return new Workspace(workRoot, pack, dimensionKey, logPrefix, hydrologyGeneratedVerification, null); } catch (Throwable failure) { + clearRuntime(); Throwable cleanupFailure = deleteWorkRoot(workRoot); if (cleanupFailure != null) { failure.addSuppressed(cleanupFailure); @@ -120,7 +132,12 @@ final class RealPackProbeSupport { } } - static List settleAndDrain() throws InterruptedException { + static List settleAndDrain(Engine engine) throws InterruptedException { + if (IrisPlatforms.isBound() && IrisPlatforms.get() instanceof StubPlatform) { + ProbeBackgroundTasks.await(engine); + awaitGenerationWorkers(); + return drainReported(); + } List drained = new ArrayList<>(); long quietSince = System.currentTimeMillis(); long start = quietSince; @@ -139,6 +156,54 @@ final class RealPackProbeSupport { return drained; } + private static void awaitGenerationWorkers() throws InterruptedException { + long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(15); + List pools = new ArrayList<>(3); + for (MultiBurst burst : List.of(MultiBurst.hydrology, MultiBurst.ioBurst, MultiBurst.burst)) { + try { + ForkJoinPool pool = burst.submit((Callable) ForkJoinTask::getPool) + .get(Math.max(1L, deadline - System.nanoTime()), TimeUnit.NANOSECONDS); + if (pool != null) { + pools.add(pool); + } + } catch (ExecutionException | TimeoutException failure) { + throw new IllegalStateException("Probe generation worker barrier failed", failure); + } + } + long[] previous = quietWorkerSnapshot(pools, deadline); + while (true) { + long[] current = quietWorkerSnapshot(pools, deadline); + if (Arrays.equals(previous, current)) { + return; + } + previous = current; + } + } + + private static long[] quietWorkerSnapshot(List pools, long deadline) throws InterruptedException { + long[] counts = new long[pools.size()]; + for (int index = 0; index < pools.size(); index++) { + ForkJoinPool pool = pools.get(index); + while (true) { + if (Thread.interrupted()) { + throw new InterruptedException("Probe generation worker barrier interrupted"); + } + if (System.nanoTime() >= deadline) { + throw new IllegalStateException("Probe generation workers did not settle within 15 seconds"); + } + long count = pool.getStealCount(); + boolean quiet = pool.isQuiescent(); + long settledCount = pool.getStealCount(); + if (quiet && count == settledCount) { + counts[index] = settledCount; + break; + } + LockSupport.parkNanos(TimeUnit.MICROSECONDS.toNanos(100)); + } + } + return counts; + } + static void printReports(String logPrefix, String label, List reports) { if (reports.isEmpty()) { return; @@ -305,7 +370,7 @@ final class RealPackProbeSupport { router.preloadActiveRuntimes(); } long readyNanos = System.nanoTime() - started; - List initializationReports = settleAndDrain(); + List initializationReports = settleAndDrain(engine); printReports(logPrefix, runLabel + " engine-init reports", initializationReports); if (bindings != null && !initializationReports.isEmpty()) { IllegalStateException failure = new IllegalStateException("Native engine initialization reported failures"); @@ -317,6 +382,7 @@ final class RealPackProbeSupport { return new EngineSession(this, data, engine, readyNanos); } catch (Throwable failure) { Throwable cleanupFailure = closeResources(engine, data); + clearRuntime(); sessionOpen = false; if (cleanupFailure != null) { failure.addSuppressed(cleanupFailure); @@ -347,8 +413,7 @@ final class RealPackProbeSupport { failure = bindingFailure; } } - IrisPlatforms.unbind(); - StubPlatform.errorSink(null); + clearRuntime(); Throwable cleanupFailure = deleteWorkRoot(workRoot); if (cleanupFailure != null) { failure = appendFailure(failure, cleanupFailure); @@ -494,6 +559,15 @@ final class RealPackProbeSupport { } } + private static void clearRuntime() { + IrisPlatforms.unbind(); + IrisServices.remove(PreservationRegistry.class); + IrisServices.remove(EngineWorldManagerProvider.class); + IrisServices.remove(EngineEffectsProvider.class); + IrisServices.remove(EnginePlatformHooks.class); + StubPlatform.errorSink(null); + } + private static void configureRuntime(File platformRoot, boolean hydrologyGeneratedVerification, RuntimeBindings bindings) { REPORTED.clear(); StubPlatform.bindGenerationStateHandlers(); diff --git a/probe/src/main/java/art/arcane/iris/probe/Terrain3DProbe.java b/probe/src/main/java/art/arcane/iris/probe/Terrain3DProbe.java index bf8233f4c..fb2d5ae9c 100644 --- a/probe/src/main/java/art/arcane/iris/probe/Terrain3DProbe.java +++ b/probe/src/main/java/art/arcane/iris/probe/Terrain3DProbe.java @@ -328,7 +328,7 @@ public final class Terrain3DProbe { sections.addAll(renderer.write()); } topology = terrainTopology.inspect(); - failReported("generation", RealPackProbeSupport.settleAndDrain()); + failReported("generation", RealPackProbeSupport.settleAndDrain(engine)); } List failures = coverageFailures(evidence, configuration.requiredBiomes(), configuration.strictGeometry()); Result result = new Result(failures.isEmpty() ? "PASS" : "FAIL", configuration, generatedChunks, diff --git a/probe/src/main/java/art/arcane/iris/probe/TransitionProbe.java b/probe/src/main/java/art/arcane/iris/probe/TransitionProbe.java index 2b299221c..8f0ac9ec5 100644 --- a/probe/src/main/java/art/arcane/iris/probe/TransitionProbe.java +++ b/probe/src/main/java/art/arcane/iris/probe/TransitionProbe.java @@ -121,7 +121,7 @@ public final class TransitionProbe { } require(historicalBiomes.equals(router.biomes().snapshot(0, 0).orElseThrow()), "Historical biome data changed after generation"); - for (Throwable failure : RealPackProbeSupport.settleAndDrain()) { + for (Throwable failure : RealPackProbeSupport.settleAndDrain(engine)) { throw new IllegalStateException("Generation reported a failure", failure); } require(bindings.captures > 0, "Promotion did not capture historical terrain"); diff --git a/probe/src/test/java/art/arcane/iris/generation/runtime/ProbeBackgroundTasksTest.java b/probe/src/test/java/art/arcane/iris/generation/runtime/ProbeBackgroundTasksTest.java new file mode 100644 index 000000000..2ee96bffc --- /dev/null +++ b/probe/src/test/java/art/arcane/iris/generation/runtime/ProbeBackgroundTasksTest.java @@ -0,0 +1,131 @@ +package art.arcane.iris.generation.runtime; + +import art.arcane.iris.probe.StubPlatform; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.testsupport.IrisRuntimeState; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.LockSupport; + +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +public final class ProbeBackgroundTasksTest { + @Rule + public final TemporaryFolder temporary = new TemporaryFolder(); + + @Before + public void bindRuntime() { + IrisPlatforms.bind(new StubPlatform(temporary.getRoot())); + } + + @After + public void resetRuntime() { + IrisRuntimeState.reset(); + StubPlatform.errorSink(null); + } + + @Test(timeout = 15_000L) + public void waitsForTrackedContinuationBeforeReturning() throws Exception { + EngineBackgroundTasks tasks = new EngineBackgroundTasks(); + tasks.openBackgroundTaskAdmission(); + CountDownLatch firstEntered = new CountDownLatch(1); + CountDownLatch firstRelease = new CountDownLatch(1); + CountDownLatch continuationEntered = new CountDownLatch(1); + CountDownLatch continuationRelease = new CountDownLatch(1); + try (ExecutorService closer = Executors.newSingleThreadExecutor()) { + try { + tasks.scheduleTrackedTask(() -> { + awaitRelease(firstEntered, firstRelease); + tasks.scheduleTrackedTask(() -> awaitRelease(continuationEntered, continuationRelease)); + }); + assertTrue(firstEntered.await(5, TimeUnit.SECONDS)); + Future completion = startDrain(closer, tasks); + assertThrows(TimeoutException.class, () -> completion.get(100, TimeUnit.MILLISECONDS)); + firstRelease.countDown(); + assertTrue(continuationEntered.await(5, TimeUnit.SECONDS)); + assertThrows(TimeoutException.class, () -> completion.get(100, TimeUnit.MILLISECONDS)); + continuationRelease.countDown(); + completion.get(5, TimeUnit.SECONDS); + } finally { + firstRelease.countDown(); + continuationRelease.countDown(); + } + } + } + + @Test(timeout = 15_000L) + public void propagatesAnAsynchronousTrackedFailure() throws Exception { + EngineBackgroundTasks tasks = new EngineBackgroundTasks(); + tasks.openBackgroundTaskAdmission(); + CountDownLatch entered = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + IllegalStateException failure = new IllegalStateException("tracked generation failure"); + CountDownLatch reported = new CountDownLatch(1); + StubPlatform.errorSink(error -> reported.countDown()); + try (ExecutorService closer = Executors.newSingleThreadExecutor()) { + try { + tasks.scheduleTrackedTask(() -> { + awaitRelease(entered, release); + throw failure; + }); + assertTrue(entered.await(5, TimeUnit.SECONDS)); + Future completion = startDrain(closer, tasks); + assertThrows(TimeoutException.class, () -> completion.get(100, TimeUnit.MILLISECONDS)); + release.countDown(); + ExecutionException observed = assertThrows(ExecutionException.class, + () -> completion.get(5, TimeUnit.SECONDS)); + assertSame(failure, observed.getCause().getCause().getCause()); + assertTrue(reported.await(5, TimeUnit.SECONDS)); + } finally { + release.countDown(); + } + } + } + + private static Future startDrain(ExecutorService closer, EngineBackgroundTasks tasks) { + AtomicReference drainingThread = new AtomicReference<>(); + Future completion = closer.submit(() -> { + drainingThread.set(Thread.currentThread()); + ProbeBackgroundTasks.await(tasks); + }); + long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5); + while (System.nanoTime() < deadline) { + Thread thread = drainingThread.get(); + if (thread != null && thread.getState() == Thread.State.TIMED_WAITING) { + for (StackTraceElement frame : thread.getStackTrace()) { + if (frame.getClassName().equals(EngineBackgroundTasks.class.getName()) + && frame.getMethodName().equals("drainBackgroundTasks")) { + return completion; + } + } + } + LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(1)); + } + throw new AssertionError("Probe barrier did not await its initial task"); + } + + private static void awaitRelease(CountDownLatch entered, CountDownLatch release) { + entered.countDown(); + try { + assertTrue(release.await(5, TimeUnit.SECONDS)); + } catch (InterruptedException interruption) { + Thread.currentThread().interrupt(); + throw new IllegalStateException(interruption); + } + } + +} diff --git a/probe/src/test/java/art/arcane/iris/probe/ObjectContinuationProbeTest.java b/probe/src/test/java/art/arcane/iris/probe/ObjectContinuationProbeTest.java index 9f83b7fc7..59defd749 100644 --- a/probe/src/test/java/art/arcane/iris/probe/ObjectContinuationProbeTest.java +++ b/probe/src/test/java/art/arcane/iris/probe/ObjectContinuationProbeTest.java @@ -211,7 +211,7 @@ public final class ObjectContinuationProbeTest { assertArrayEquals(oldReceipt, SavedTerrainChunk.readReceipt(world.worldFolder().toPath(), 0, 0)); assertEquals(originalActivation, reopened.resolveActivation(0, 0).activationId()); assertTrue(reopened.semantics(1, 0).orElseThrow().objectKeys().contains(fixture.objectKey())); - assertTrue("Generation must not report swallowed errors", RealPackProbeSupport.settleAndDrain().isEmpty()); + assertTrue("Generation must not report swallowed errors", RealPackProbeSupport.settleAndDrain(engine).isEmpty()); } finally { engine.close(); } diff --git a/probe/src/test/java/art/arcane/iris/probe/RealPackProbeSupportBarrierTest.java b/probe/src/test/java/art/arcane/iris/probe/RealPackProbeSupportBarrierTest.java new file mode 100644 index 000000000..a920f8cb7 --- /dev/null +++ b/probe/src/test/java/art/arcane/iris/probe/RealPackProbeSupportBarrierTest.java @@ -0,0 +1,101 @@ +package art.arcane.iris.probe; + +import art.arcane.iris.generation.concurrent.MultiBurst; +import art.arcane.iris.generation.runtime.EngineEffectsProvider; +import art.arcane.iris.generation.runtime.EnginePlatformHooks; +import art.arcane.iris.generation.runtime.EngineWorldManagerProvider; +import art.arcane.iris.generation.runtime.PreservationRegistry; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.spi.IrisServices; +import art.arcane.iris.testsupport.IrisRuntimeState; +import art.arcane.iris.testsupport.PlatformLeakGuard; +import art.arcane.iris.world.task.J; +import org.junit.AfterClass; +import org.junit.Test; + +import java.io.File; +import java.net.URL; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +public final class RealPackProbeSupportBarrierTest { + @AfterClass + public static void resetRuntime() { + IrisRuntimeState.reset(); + } + + @Test + public void closesOwnedRuntimeServicesAndPreservesUnrelatedServices() throws Exception { + URL resource = RealPackProbeSupportBarrierTest.class.getResource("/seed-cohesion"); + assertNotNull(resource); + IrisServices.register(String.class, "unrelated"); + try { + try (RealPackProbeSupport.Workspace workspace = RealPackProbeSupport.openWorkspace( + new File(resource.toURI()), "cohesion", "[workspace-cleanup]")) { + assertNotNull(IrisServices.getOrNull(EngineEffectsProvider.class)); + } + assertFalse(IrisPlatforms.isBound()); + assertNull(IrisServices.getOrNull(PreservationRegistry.class)); + assertNull(IrisServices.getOrNull(EngineWorldManagerProvider.class)); + assertNull(IrisServices.getOrNull(EngineEffectsProvider.class)); + assertNull(IrisServices.getOrNull(EnginePlatformHooks.class)); + assertEquals("unrelated", IrisServices.get(String.class)); + } finally { + IrisServices.remove(String.class); + } + } + + @Test(timeout = 30_000L) + public void waitsForCrossPoolContinuationsAndCollectsTheirErrors() throws Exception { + URL resource = RealPackProbeSupportBarrierTest.class.getResource("/seed-cohesion"); + assertNotNull(resource); + try (RealPackProbeSupport.Workspace workspace = RealPackProbeSupport.openWorkspace( + new File(resource.toURI()), "cohesion", "[worker-barrier]"); + RealPackProbeSupport.EngineSession session = workspace.openEngine(1337L, false, "barrier")) { + for (int repeat = 0; repeat < 40; repeat++) { + verifyCrossPoolContinuation(session, MultiBurst.ioBurst, MultiBurst.hydrology); + verifyCrossPoolContinuation(session, MultiBurst.hydrology, MultiBurst.ioBurst); + } + } + PlatformLeakGuard.requireClean("closed real-pack workspace", "end"); + } + + private static void verifyCrossPoolContinuation(RealPackProbeSupport.EngineSession session, + MultiBurst first, MultiBurst second) throws Exception { + IllegalStateException failure = new IllegalStateException("nested generation failure"); + CountDownLatch entered = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + try { + first.lazy(() -> { + entered.countDown(); + try { + assertTrue(release.await(5, TimeUnit.SECONDS)); + } catch (InterruptedException interruption) { + Thread.currentThread().interrupt(); + throw new IllegalStateException(interruption); + } + Runnable failedTask = () -> { + throw failure; + }; + second.lazy(() -> first.lazy(() -> second.lazy(() -> J.a(failedTask)))); + }); + assertTrue(entered.await(5, TimeUnit.SECONDS)); + release.countDown(); + List reports = RealPackProbeSupport.settleAndDrain(session.engine()); + assertEquals(1, reports.size()); + assertSame(failure, reports.getFirst()); + assertTrue(RealPackProbeSupport.drainReported().isEmpty()); + } finally { + release.countDown(); + } + } + +}