Adapter patches

(tests)
This commit is contained in:
Brian Neumann-Fopiano
2026-10-04 13:08:41 -04:00
parent 4b4b418c61
commit a6149b27c9
40 changed files with 894 additions and 211 deletions
@@ -14,6 +14,7 @@ import net.minecraft.SharedConstants;
import net.minecraft.server.Bootstrap; import net.minecraft.server.Bootstrap;
import net.minecraft.world.level.LevelHeightAccessor; import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.levelgen.Heightmap; import net.minecraft.world.level.levelgen.Heightmap;
import org.junit.AfterClass;
import org.junit.BeforeClass; import org.junit.BeforeClass;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
@@ -45,6 +46,11 @@ public final class IrisChunkGeneratorHeightCacheTest {
NativeBlockTags.bindHeightmapFixtures(); NativeBlockTags.bindHeightmapFixtures();
} }
@AfterClass
public static void restoreBlockTags() {
NativeBlockTags.restoreTags();
}
@Test @Test
public void repeatedNativeHeightUsesLiveHeightQueriesAcrossRuntimeChanges() throws Exception { public void repeatedNativeHeightUsesLiveHeightQueriesAcrossRuntimeChanges() throws Exception {
Fixture fixture = fixture(); Fixture fixture = fixture();
@@ -20,6 +20,7 @@ import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.RotatedPillarBlock; import net.minecraft.world.level.block.RotatedPillarBlock;
import net.minecraft.world.level.block.LiquidBlock; import net.minecraft.world.level.block.LiquidBlock;
import net.minecraft.world.level.levelgen.Heightmap; import net.minecraft.world.level.levelgen.Heightmap;
import org.junit.AfterClass;
import org.junit.BeforeClass; import org.junit.BeforeClass;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
@@ -46,6 +47,11 @@ public final class NativeTransitionColumnTest {
NativeBlockTags.bindHeightmapFixtures(); NativeBlockTags.bindHeightmapFixtures();
} }
@AfterClass
public static void restoreBlockTags() {
NativeBlockTags.restoreTags();
}
@Test @Test
public void customKeysUseCarrierForNativeColumnsAndHeightmaps() { public void customKeysUseCarrierForNativeColumnsAndHeightmaps() {
String key = "itemsadder:rocks/ruby_ore"; String key = "itemsadder:rocks/ruby_ore";
@@ -1,21 +1,30 @@
package art.arcane.iris.nativegen.v26_3_R1; package art.arcane.iris.nativegen.v26_3_R1;
import net.minecraft.core.Holder; import net.minecraft.core.Holder;
import net.minecraft.core.HolderSet;
import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries; import net.minecraft.core.registries.Registries;
import net.minecraft.tags.BlockTags; import net.minecraft.tags.BlockTags;
import net.minecraft.tags.TagLoader; import net.minecraft.tags.TagLoader;
import net.minecraft.tags.TagKey;
import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.Blocks;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
public final class NativeBlockTags { public final class NativeBlockTags {
private static Map<TagKey<Block>, List<Holder<Block>>> originalTags;
private NativeBlockTags() { private NativeBlockTags() {
} }
public static void bindHeightmapFixtures() { public static void bindHeightmapFixtures() {
originalTags = new HashMap<>();
for (HolderSet.Named<Block> tag : BuiltInRegistries.BLOCK.getTags().toList()) {
originalTags.put(tag.key(), tag.stream().toList());
}
List<Holder<Block>> solid = List.of(Blocks.STONE.builtInRegistryHolder(), List<Holder<Block>> solid = List.of(Blocks.STONE.builtInRegistryHolder(),
Blocks.OAK_LOG.builtInRegistryHolder(), Blocks.SPRUCE_LOG.builtInRegistryHolder()); Blocks.OAK_LOG.builtInRegistryHolder(), Blocks.SPRUCE_LOG.builtInRegistryHolder());
List<Holder<Block>> canopy = List.of(Blocks.STONE.builtInRegistryHolder(), List<Holder<Block>> 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, canopy,
BlockTags.BLOCKS_MOTION_IN_HEIGHTMAP_NO_LEAVES, solid))).apply(); 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;
}
} }
@@ -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.IglooPieces;
import net.minecraft.world.level.levelgen.structure.structures.SwampHutPiece; import net.minecraft.world.level.levelgen.structure.structures.SwampHutPiece;
import net.minecraft.world.level.levelgen.structure.structures.SwampHutStructure; import net.minecraft.world.level.levelgen.structure.structures.SwampHutStructure;
import org.junit.AfterClass;
import org.junit.BeforeClass; import org.junit.BeforeClass;
import org.junit.Test; import org.junit.Test;
@@ -68,6 +69,11 @@ public class WorldgenTerrainHeightmapsTest {
NativeBlockTags.bindHeightmapFixtures(); NativeBlockTags.bindHeightmapFixtures();
} }
@AfterClass
public static void restoreBlockTags() {
NativeBlockTags.restoreTags();
}
@Test @Test
public void authoringHeightmapsReadPlacedObjectsAndFluidsWithoutTerrainResolvers() { public void authoringHeightmapsReadPlacedObjectsAndFluidsWithoutTerrainResolvers() {
ProtoChunk chunk = terrainChunk(new ChunkPos(0, 0)); ProtoChunk chunk = terrainChunk(new ChunkPos(0, 0));
@@ -13,6 +13,7 @@ import com.google.gson.JsonParser;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.junit.AfterClass; import org.junit.AfterClass;
import org.junit.BeforeClass; import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Test; import org.junit.Test;
import java.io.File; import java.io.File;
@@ -26,6 +27,9 @@ import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertThrows;
public final class BareMobSpawnHistoryTest { 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 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 String NATURAL_MOB_SPAWNS = "minecraft:gameplay/natural_mob_spawns";
private static final GenerationRegistryContract.PhysicalResourceKey INSTALLED = private static final GenerationRegistryContract.PhysicalResourceKey INSTALLED =
@@ -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.levelgen.structure.templatesystem.StructureTemplateManager;
import net.minecraft.world.level.storage.LevelStorageSource; import net.minecraft.world.level.storage.LevelStorageSource;
import org.junit.Rule; import org.junit.Rule;
import org.junit.ClassRule;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.TemporaryFolder; import org.junit.rules.TemporaryFolder;
import org.bukkit.craftbukkit.generator.CustomChunkGenerator; import org.bukkit.craftbukkit.generator.CustomChunkGenerator;
@@ -49,6 +50,9 @@ import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
public class HeadlessChunkGeneratorTest { public class HeadlessChunkGeneratorTest {
@ClassRule
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
@Rule @Rule
public TemporaryFolder temporary = new TemporaryFolder(); public TemporaryFolder temporary = new TemporaryFolder();
@@ -31,6 +31,7 @@ import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemp
import net.minecraft.world.level.storage.LevelStorageSource; import net.minecraft.world.level.storage.LevelStorageSource;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.junit.Rule; import org.junit.Rule;
import org.junit.ClassRule;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.TemporaryFolder; import org.junit.rules.TemporaryFolder;
@@ -48,6 +49,9 @@ import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
public final class HeadlessChunkLightingTest { public final class HeadlessChunkLightingTest {
@ClassRule
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
@Rule @Rule
public TemporaryFolder temporary = new TemporaryFolder(); public TemporaryFolder temporary = new TemporaryFolder();
@@ -38,6 +38,7 @@ import net.minecraft.world.ticks.ScheduledTick;
import org.bukkit.NamespacedKey; import org.bukkit.NamespacedKey;
import org.bukkit.persistence.PersistentDataType; import org.bukkit.persistence.PersistentDataType;
import org.junit.Rule; import org.junit.Rule;
import org.junit.ClassRule;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.TemporaryFolder; import org.junit.rules.TemporaryFolder;
@@ -54,6 +55,9 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
public class HeadlessChunkSerializationTest { public class HeadlessChunkSerializationTest {
@ClassRule
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
@Rule @Rule
public TemporaryFolder temporary = new TemporaryFolder(); public TemporaryFolder temporary = new TemporaryFolder();
@@ -9,6 +9,7 @@ import net.minecraft.world.item.Items;
import java.util.Map; import java.util.Map;
import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.Blocks;
import org.junit.ClassRule;
import org.junit.Test; import org.junit.Test;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
@@ -18,6 +19,9 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
public final class HeadlessNativeBootstrapTest { public final class HeadlessNativeBootstrapTest {
@ClassRule
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
@Test @Test
public void bindsNativeItemComponentsWithoutStartingServer() throws Exception { public void bindsNativeItemComponentsWithoutStartingServer() throws Exception {
HeadlessNativeBootstrap.bindComponents(HeadlessNativeTestRegistries.get()); HeadlessNativeBootstrap.bindComponents(HeadlessNativeTestRegistries.get());
@@ -7,12 +7,16 @@ import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceKey;
import net.minecraft.world.level.biome.Biome; import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.levelgen.structure.Structure; import net.minecraft.world.level.levelgen.structure.Structure;
import org.junit.ClassRule;
import org.junit.Test; import org.junit.Test;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
public final class HeadlessNativeRegistriesTest { public final class HeadlessNativeRegistriesTest {
@ClassRule
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
@Test @Test
public void nativeStructureBiomeTagsUseTheLoadedBiomeHolders() throws Exception { public void nativeStructureBiomeTagsUseTheLoadedBiomeHolders() throws Exception {
RegistryAccess registries = HeadlessNativeTestRegistries.get(); RegistryAccess registries = HeadlessNativeTestRegistries.get();
@@ -4,6 +4,7 @@ import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeRegistryAccess;
import net.minecraft.core.registries.Registries; import net.minecraft.core.registries.Registries;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.junit.BeforeClass; import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Test; import org.junit.Test;
import java.util.Map; import java.util.Map;
@@ -17,6 +18,9 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertThrows;
public final class HeadlessNativeRegistryTest { public final class HeadlessNativeRegistryTest {
@ClassRule
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
@BeforeClass @BeforeClass
public static void bootstrap() { public static void bootstrap() {
HeadlessNativeBootstrap.initialize(); HeadlessNativeBootstrap.initialize();
@@ -11,6 +11,7 @@ import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeTileReader;
import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.CompoundTag;
import net.minecraft.data.registries.VanillaRegistries; import net.minecraft.data.registries.VanillaRegistries;
import org.junit.BeforeClass; import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Test; import org.junit.Test;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
@@ -23,6 +24,9 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertThrows;
public final class HeadlessNativeTileTest { public final class HeadlessNativeTileTest {
@ClassRule
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
@BeforeClass @BeforeClass
public static void bootstrap() { public static void bootstrap() {
HeadlessNativeBootstrap.initialize(); HeadlessNativeBootstrap.initialize();
@@ -17,6 +17,7 @@ import org.bukkit.Bukkit;
import org.bukkit.NamespacedKey; import org.bukkit.NamespacedKey;
import org.bukkit.persistence.PersistentDataType; import org.bukkit.persistence.PersistentDataType;
import org.junit.Rule; import org.junit.Rule;
import org.junit.ClassRule;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.TemporaryFolder; import org.junit.rules.TemporaryFolder;
import org.spigotmc.SpigotWorldConfig; import org.spigotmc.SpigotWorldConfig;
@@ -36,6 +37,9 @@ import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
public final class HeadlessRegionTerrainTest { public final class HeadlessRegionTerrainTest {
@ClassRule
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
@Rule @Rule
public TemporaryFolder temporary = new TemporaryFolder(); public TemporaryFolder temporary = new TemporaryFolder();
@@ -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.RegionFile;
import net.minecraft.world.level.chunk.storage.RegionStorageInfo; import net.minecraft.world.level.chunk.storage.RegionStorageInfo;
import org.junit.BeforeClass; import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Test; import org.junit.Test;
import java.io.DataInputStream; import java.io.DataInputStream;
@@ -38,6 +39,9 @@ import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
public final class NativeRegionTerrainWriterTest { public final class NativeRegionTerrainWriterTest {
@ClassRule
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
@BeforeClass @BeforeClass
public static void bootstrap() { public static void bootstrap() {
SharedConstants.tryDetectVersion(); SharedConstants.tryDetectVersion();
@@ -26,6 +26,7 @@ import net.minecraft.world.level.material.Fluids;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.junit.AfterClass; import org.junit.AfterClass;
import org.junit.BeforeClass; import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Test; import org.junit.Test;
import java.util.ArrayList; import java.util.ArrayList;
@@ -40,6 +41,9 @@ import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
public final class OfflineNativeFeatureFeasibilityTest { public final class OfflineNativeFeatureFeasibilityTest {
@ClassRule
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
private static HeadlessNativeRegistries loaded; private static HeadlessNativeRegistries loaded;
private static RegistryAccess registries; private static RegistryAccess registries;
private static DimensionType dimension; private static DimensionType dimension;
@@ -1,6 +1,7 @@
package art.arcane.iris.probe; package art.arcane.iris.probe;
import org.junit.Rule; import org.junit.Rule;
import org.junit.ClassRule;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.TemporaryFolder; import org.junit.rules.TemporaryFolder;
import org.mockito.MockedStatic; import org.mockito.MockedStatic;
@@ -22,6 +23,9 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.mockStatic;
public final class OfflineRegionPublicationTest { public final class OfflineRegionPublicationTest {
@ClassRule
public static final PaperRegistryIsolation registryIsolation = new PaperRegistryIsolation();
@Rule @Rule
public TemporaryFolder temporary = new TemporaryFolder(); public TemporaryFolder temporary = new TemporaryFolder();
@@ -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<Object, Object> registries = registries();
Map<Object, Object> previous = new HashMap<>(registries);
registries.clear();
try {
base.evaluate();
} finally {
registries.clear();
registries.putAll(previous);
}
}
};
}
@SuppressWarnings("unchecked")
private static Map<Object, Object> registries() throws ReflectiveOperationException {
Field field = PaperRegistryAccess.class.getDeclaredField("registries");
field.setAccessible(true);
return (Map<Object, Object>) field.get(PaperRegistryAccess.instance());
}
}
+15 -4
View File
@@ -96,7 +96,7 @@ String forgeArtifactName = irisArtifactName('Forge', "${minecraftVersion}+${load
String neoForgeArtifactName = irisArtifactName('NeoForge', "${minecraftVersion}+${loaderDisplayVersion(neoForgeVersion)}") String neoForgeArtifactName = irisArtifactName('NeoForge', "${minecraftVersion}+${loaderDisplayVersion(neoForgeVersion)}")
Map<String, String> runtimeDownloadRelocations = (Map<String, String>) ext.runtimeDownloadRelocations Map<String, String> runtimeDownloadRelocations = (Map<String, String>) ext.runtimeDownloadRelocations
List<String> runtimeDownloadedPrefixes = (List<String>) ext.runtimeDownloadedPrefixes List<String> runtimeDownloadedPrefixes = (List<String>) 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 legacyBukkitJar = layout.buildDirectory.file("libs/Iris-${project.version}.jar").get().asFile
File bukkitArtifact = layout.buildDirectory.file("libs/${bukkitArtifactName}").get().asFile File bukkitArtifact = layout.buildDirectory.file("libs/${bukkitArtifactName}").get().asFile
File consumerBukkitArtifact = layout.buildDirectory.file("consumer/${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. // moves. Without a local checkout the Exec tasks always run and the nested build decides what is stale.
Closure<Void> declareVolmLibInputs = { Exec task -> Closure<Void> declareVolmLibInputs = { Exec task ->
task.inputs.property('volmLibStamp', volmLibStamp) 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) { if (localVolmLibCheckout == null) {
task.outputs.upToDateWhen { false } task.outputs.upToDateWhen { false }
} }
@@ -781,9 +784,20 @@ tasks.register('irisDev', Copy) {
dependsOn(':core:javadocJar') 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 { allprojects {
apply plugin: 'java' 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 { java {
toolchain { toolchain {
languageVersion = JavaLanguageVersion.of(25) languageVersion = JavaLanguageVersion.of(25)
@@ -979,9 +993,6 @@ project(':adapters:bukkit:nms:v26_3_R1') {
} }
sourceSets.test.compileClasspath += sourceSets.offline.output sourceSets.test.compileClasspath += sourceSets.offline.output
sourceSets.test.runtimeClasspath += sourceSets.offline.output sourceSets.test.runtimeClasspath += sourceSets.offline.output
tasks.named('test', Test).configure {
forkEvery = 1
}
TaskProvider<Jar> offlineJar = tasks.register('offlineJar', Jar) { TaskProvider<Jar> offlineJar = tasks.register('offlineJar', Jar) {
dependsOn(offlineClasses) dependsOn(offlineClasses)
-4
View File
@@ -170,12 +170,8 @@ dependencies {
testImplementation(libs.multiverseCore) 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 { tasks.named('test').configure {
maxHeapSize = '1g' maxHeapSize = '1g'
maxParallelForks = coreTestForks
jvmArgs('-XX:+ExitOnOutOfMemoryError') jvmArgs('-XX:+ExitOnOutOfMemoryError')
} }
@@ -1,6 +1,8 @@
package art.arcane.iris.generation.decoration; package art.arcane.iris.generation.decoration;
import art.arcane.iris.generation.block.IrisBlockData; 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.IrisGeneratorStyle;
import art.arcane.iris.generation.noise.NoiseStyle; import art.arcane.iris.generation.noise.NoiseStyle;
import art.arcane.iris.generation.terrain.IrisMaterialPalette; 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.assertFalse;
import static org.junit.Assert.assertNull; import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame; 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 { public class IrisOreBandsTest {
private static final RNG RNG_SOURCE = new RNG(77L); private static final RNG RNG_SOURCE = new RNG(77L);
@@ -69,10 +66,8 @@ public class IrisOreBandsTest {
@Test @Test
public void constantChanceMatchesSamplingTheFlatField() { public void constantChanceMatchesSamplingTheFlatField() {
IrisOreGenerator flat = new IrisOreGenerator(); IrisOreGenerator flat = new IrisOreGenerator();
IrisMaterialPalette palette = mock(IrisMaterialPalette.class); NativeBlockState ore = new KeyedBlockState("minecraft:iron_ore");
NativeBlockState ore = mock(NativeBlockState.class); IrisMaterialPalette palette = new FixedPalette(ore);
when(palette.getPalette()).thenReturn(new KList<>(mock(IrisBlockData.class)));
when(palette.get(any(), anyDouble(), anyDouble(), anyDouble(), isNull())).thenReturn(ore);
IrisGeneratorStyle style = new IrisGeneratorStyle(NoiseStyle.FLAT).setZoom(2.5D).setExponent(1.3D) IrisGeneratorStyle style = new IrisGeneratorStyle(NoiseStyle.FLAT).setZoom(2.5D).setExponent(1.3D)
.setFracture(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).setMultiplier(9D)); .setFracture(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).setMultiplier(9D));
flat.setPalette(palette).setChanceStyle(style).setRange(new IrisRange(0, 100)); 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) { private static IrisOreGenerator generator(double min, double max, double threshold, long seed) {
IrisOreGenerator generator = new IrisOreGenerator(); IrisOreGenerator generator = new IrisOreGenerator();
IrisMaterialPalette palette = mock(IrisMaterialPalette.class); NativeBlockState state = new KeyedBlockState("minecraft:iron_ore");
NativeBlockState state = mock(NativeBlockState.class); IrisMaterialPalette palette = new FixedPalette(state);
when(palette.getPalette()).thenReturn(new KList<>(mock(IrisBlockData.class))); IrisGeneratorStyle style = new FixedChanceStyle(new CoordinateChance(seed));
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);
return generator.setPalette(palette).setChanceStyle(style).setThreshold(threshold) return generator.setPalette(palette).setChanceStyle(style).setThreshold(threshold)
.setRange(new IrisRange(min, max)); .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)); 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); 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();
}
}
} }
@@ -11,6 +11,7 @@ import java.security.NoSuchAlgorithmException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.HexFormat;
import java.util.Map; import java.util.Map;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
@@ -27,7 +28,7 @@ public class HydrologyPlanHashTest {
@Test @Test
public void fullTilePlansAreByteIdentical() { public void fullTilePlansAreByteIdentical() {
StringBuilder canonical = new StringBuilder(); MessageDigest canonical = sha256();
int courses = 0; int courses = 0;
int columns = 0; int columns = 0;
for (long seed : new long[]{77L, 19L}) { for (long seed : new long[]{77L, 19L}) {
@@ -37,16 +38,16 @@ public class HydrologyPlanHashTest {
HydrologyTile tile = planner.plan(key); HydrologyTile tile = planner.plan(key);
courses += tile.courses().size(); courses += tile.courses().size();
columns += tile.footprint().columns().size(); columns += tile.footprint().columns().size();
canonical.append(render(tile)); render(tile, canonical);
} }
} }
assertTrue("planned courses: " + courses, courses > 0); assertTrue("planned courses: " + courses, courses > 0);
assertTrue("planned columns: " + columns, columns > 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) { private static void render(HydrologyTile tile, MessageDigest digest) {
StringBuilder out = new StringBuilder(); CanonicalDigest out = new CanonicalDigest(digest);
out.append("tile ").append(tile.key()).append('\n'); out.append("tile ").append(tile.key()).append('\n');
out.append("seed ").append(tile.worldSeed()).append('\n'); out.append("seed ").append(tile.worldSeed()).append('\n');
out.append("fingerprint ").append(tile.settingsFingerprint()).append('\n'); out.append("fingerprint ").append(tile.settingsFingerprint()).append('\n');
@@ -78,23 +79,14 @@ public class HydrologyPlanHashTest {
for (Map.Entry<Long, HydrologyColumnSample> column : columns) { for (Map.Entry<Long, HydrologyColumnSample> column : columns) {
out.append("column ").append(column.getKey()).append(' ').append(column.getValue()).append('\n'); out.append("column ").append(column.getKey()).append(' ').append(column.getValue()).append('\n');
} }
return out.toString();
} }
private static String digest(String value) { private static MessageDigest sha256() {
MessageDigest sha256;
try { try {
sha256 = MessageDigest.getInstance("SHA-256"); return MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) { } catch (NoSuchAlgorithmException failure) {
throw new IllegalStateException(e); 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) { private static HydrologyTerrainSampler rollingCoast(int coastX) {
@@ -230,4 +222,22 @@ public class HydrologyPlanHashTest {
HydrologyPlannerSettings.SurfacePolicyBounds.NONE 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;
}
}
} }
@@ -23,6 +23,7 @@ import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static org.mockito.Mockito.withSettings;
public class CaveTerrainSnapshotTest { public class CaveTerrainSnapshotTest {
private static final int SECTIONS = 5; private static final int SECTIONS = 5;
@@ -176,7 +177,7 @@ public class CaveTerrainSnapshotTest {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private static MantleChunk<Matter> chunk(Matter[] sections) { private static MantleChunk<Matter> chunk(Matter[] sections) {
MantleChunk<Matter> chunk = mock(MantleChunk.class); MantleChunk<Matter> chunk = mock(MantleChunk.class, withSettings().stubOnly());
when(chunk.sectionCount()).thenReturn(sections.length); when(chunk.sectionCount()).thenReturn(sections.length);
when(chunk.exists(anyInt())).thenAnswer(call -> { when(chunk.exists(anyInt())).thenAnswer(call -> {
int section = call.getArgument(0); int section = call.getArgument(0);
@@ -20,6 +20,7 @@ import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.matter.Matter; import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.matter.MatterCavern; import art.arcane.volmlib.util.matter.MatterCavern;
import art.arcane.volmlib.util.matter.MatterSlice; import art.arcane.volmlib.util.matter.MatterSlice;
import art.arcane.volmlib.util.matter.slices.CavernMatter;
import org.junit.BeforeClass; import org.junit.BeforeClass;
import org.junit.Test; 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.doAnswer;
import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.withSettings;
public class IrisCaveCarver3DNearParityTest { public class IrisCaveCarver3DNearParityTest {
private static final long CARVE_SEED = -54_777_863_784_492_918L; 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)); createProfile(false, false).setDetailWeight(0D).setAllowFluid(false));
AtomicInteger calls = new AtomicInteger(); AtomicInteger calls = new AtomicInteger();
Set<Integer> sampledY = new HashSet<>(); Set<Integer> sampledY = new HashSet<>();
CNG density = mock(CNG.class); CNG density = mock(CNG.class, withSettings().stubOnly());
doAnswer(invocation -> { doAnswer(invocation -> {
calls.incrementAndGet(); calls.incrementAndGet();
double x = invocation.getArgument(0); double x = invocation.getArgument(0);
@@ -193,14 +195,14 @@ public class IrisCaveCarver3DNearParityTest {
@Test @Test
public void expressionStylesKeepTheOriginalSamplingAndCarvingBehavior() throws Exception { public void expressionStylesKeepTheOriginalSamplingAndCarvingBehavior() throws Exception {
CNG density = mock(CNG.class); CNG density = mock(CNG.class, withSettings().stubOnly());
AtomicInteger calls = new AtomicInteger(); AtomicInteger calls = new AtomicInteger();
doAnswer(invocation -> { doAnswer(invocation -> {
calls.incrementAndGet(); calls.incrementAndGet();
double y = invocation.getArgument(1); double y = invocation.getArgument(1);
return Math.sin(y * 0.2D); return Math.sin(y * 0.2D);
}).when(density).noiseFastSigned3D(anyDouble(), anyDouble(), anyDouble()); }).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_expression").when(style).getExpression();
doReturn(density).when(style).create(any(RNG.class), any(IrisData.class)); doReturn(density).when(style).create(any(RNG.class), any(IrisData.class));
IrisCaveProfile profile = createProfile(false, false).setBaseDensityStyle(style) IrisCaveProfile profile = createProfile(false, false).setBaseDensityStyle(style)
@@ -240,10 +242,10 @@ public class IrisCaveCarver3DNearParityTest {
.setModules(new KList<>(new IrisCaveFieldModule(new IrisGeneratorStyle(NoiseStyle.FLAT), .setModules(new KList<>(new IrisCaveFieldModule(new IrisGeneratorStyle(NoiseStyle.FLAT),
1D, 0D, new IrisRange(5D, 6D), false))); 1D, 0D, new IrisRange(5D, 6D), false)));
IrisCaveCarver3D carver = new IrisCaveCarver3D(engine, profile); 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()); doReturn(1D).when(base).noiseFastSigned3D(anyDouble(), anyDouble(), anyDouble());
baseDensityField.set(carver, base); baseDensityField.set(carver, base);
CNG module = mock(CNG.class); CNG module = mock(CNG.class, withSettings().stubOnly());
doReturn(-1D).when(module).noiseFastSigned3D(anyDouble(), anyDouble(), anyDouble()); doReturn(-1D).when(module).noiseFastSigned3D(anyDouble(), anyDouble(), anyDouble());
moduleDensityField.set(((CaveFieldModuleState[]) modulesField.get(carver))[0], module); moduleDensityField.set(((CaveFieldModuleState[]) modulesField.get(carver))[0], module);
WriterCapture capture = createWriterCapture(128); WriterCapture capture = createWriterCapture(128);
@@ -307,7 +309,7 @@ public class IrisCaveCarver3DNearParityTest {
WriterCapture capture = createWriterCapture(128); WriterCapture capture = createWriterCapture(128);
if (fail) { if (fail) {
AtomicInteger samples = new AtomicInteger(); AtomicInteger samples = new AtomicInteger();
CNG density = mock(CNG.class); CNG density = mock(CNG.class, withSettings().stubOnly());
doAnswer(invocation -> { doAnswer(invocation -> {
if (samples.incrementAndGet() > 300) { if (samples.incrementAndGet() > 300) {
throw new IllegalStateException("Density sample failed"); throw new IllegalStateException("Density sample failed");
@@ -380,7 +382,7 @@ public class IrisCaveCarver3DNearParityTest {
.setDetailWeight(0D) .setDetailWeight(0D)
.setDensityThreshold(new IrisStyledRange(-2D, -2D, new IrisGeneratorStyle(NoiseStyle.FLAT))); .setDensityThreshold(new IrisStyledRange(-2D, -2D, new IrisGeneratorStyle(NoiseStyle.FLAT)));
IrisCaveCarver3D carver = new IrisCaveCarver3D(engine, profile); IrisCaveCarver3D carver = new IrisCaveCarver3D(engine, profile);
CNG detail = mock(CNG.class); CNG detail = mock(CNG.class, withSettings().stubOnly());
doAnswer(invocation -> { doAnswer(invocation -> {
throw new AssertionError("Rejected voxel sampled an aquifer"); throw new AssertionError("Rejected voxel sampled an aquifer");
}).when(detail).noiseFastSigned3D(anyDouble(), anyDouble(), anyDouble()); }).when(detail).noiseFastSigned3D(anyDouble(), anyDouble(), anyDouble());
@@ -1343,10 +1345,10 @@ public class IrisCaveCarver3DNearParityTest {
} }
private Engine createEngine(int worldHeight, int sampledHeight) { private Engine createEngine(int worldHeight, int sampledHeight) {
Engine engine = mock(Engine.class); Engine engine = mock(Engine.class, withSettings().stubOnly());
IrisData data = mock(IrisData.class); IrisData data = mock(IrisData.class, withSettings().stubOnly());
IrisDimension dimension = mock(IrisDimension.class); IrisDimension dimension = mock(IrisDimension.class, withSettings().stubOnly());
SeedManager seedManager = mock(SeedManager.class); SeedManager seedManager = mock(SeedManager.class, withSettings().stubOnly());
EngineMetrics metrics = new EngineMetrics(16); EngineMetrics metrics = new EngineMetrics(16);
IrisWorld world = IrisWorld.builder().minHeight(0).maxHeight(worldHeight).build(); IrisWorld world = IrisWorld.builder().minHeight(0).maxHeight(worldHeight).build();
@@ -1467,15 +1469,15 @@ public class IrisCaveCarver3DNearParityTest {
} }
private WriterCapture createWriterCapture(int worldHeight) { private WriterCapture createWriterCapture(int worldHeight) {
MantleWriter writer = mock(MantleWriter.class); MantleWriter writer = mock(MantleWriter.class, withSettings().stubOnly());
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
Mantle<Matter> mantle = mock(Mantle.class); Mantle<Matter> mantle = mock(Mantle.class, withSettings().stubOnly());
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
MantleChunk<Matter> chunk = mock(MantleChunk.class); MantleChunk<Matter> chunk = mock(MantleChunk.class, withSettings().stubOnly());
Map<Integer, Matter> sections = new HashMap<>(); Map<Integer, Matter> sections = new HashMap<>();
Map<Integer, Map<Integer, MatterCavern>> sectionCells = new HashMap<>();
Set<String> carvedCells = new HashSet<>(); Set<String> carvedCells = new HashSet<>();
Map<String, Byte> carvedLiquids = new HashMap<>(); Map<String, Byte> carvedLiquids = new HashMap<>();
WriterCapture capture = new WriterCapture(writer, carvedCells, carvedLiquids);
doReturn(mantle).when(writer).getMantle(); doReturn(mantle).when(writer).getMantle();
doReturn(worldHeight).when(mantle).getWorldHeight(); doReturn(worldHeight).when(mantle).getWorldHeight();
@@ -1488,41 +1490,19 @@ public class IrisCaveCarver3DNearParityTest {
return section; return section;
} }
Matter created = createSection(sectionIndex, sectionCells, carvedCells, carvedLiquids); Matter created = createSection(sectionIndex, capture);
sections.put(sectionIndex, created); sections.put(sectionIndex, created);
return created; return created;
}).when(chunk).getOrCreate(anyInt()); }).when(chunk).getOrCreate(anyInt());
return new WriterCapture(writer, carvedCells, carvedLiquids); return capture;
} }
private Matter createSection(int sectionIndex, Map<Integer, Map<Integer, MatterCavern>> sectionCells, Set<String> carvedCells, Map<String, Byte> carvedLiquids) { private Matter createSection(int sectionIndex, WriterCapture capture) {
Matter matter = mock(Matter.class); Matter matter = mock(Matter.class, withSettings().stubOnly());
@SuppressWarnings("unchecked") MatterSlice<MatterCavern> slice = new CapturingCavernMatter(sectionIndex, capture);
MatterSlice<MatterCavern> slice = mock(MatterSlice.class);
Map<Integer, MatterCavern> localCells = sectionCells.computeIfAbsent(sectionIndex, key -> new HashMap<>());
doReturn(slice).when(matter).slice(MatterCavern.class); doReturn(slice).when(matter).slice(MatterCavern.class);
doReturn(slice).when(matter).getSlice(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; return matter;
} }
@@ -1649,10 +1629,6 @@ public class IrisCaveCarver3DNearParityTest {
return (value * 2D) - 1D; 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) { private String cellKey(int x, int y, int z) {
return x + ":" + y + ":" + 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 static final class WriterCapture {
private final MantleWriter writer; private final MantleWriter writer;
private final Set<String> carvedCells; private final Set<String> carvedCells;
@@ -15,39 +15,43 @@ import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap; import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.mantle.runtime.Mantle; import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk; 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.Matter;
import art.arcane.volmlib.util.matter.MatterCavern; import art.arcane.volmlib.util.matter.MatterCavern;
import art.arcane.volmlib.util.stream.ProceduralStream; import art.arcane.volmlib.util.stream.ProceduralStream;
import art.arcane.volmlib.util.stream.interpolation.Interpolated;
import org.junit.BeforeClass; import org.junit.BeforeClass;
import org.junit.Test; import org.junit.Test;
import org.mockito.MockedStatic; import org.mockito.MockedStatic;
import java.util.List; import java.util.List;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.Random; import java.util.Random;
import static org.junit.Assert.assertSame; 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.anyDouble;
import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq; 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.mock;
import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static org.mockito.Mockito.withSettings;
public class EngineBiomeColumnTest { public class EngineBiomeColumnTest {
private static final int SAMPLES = 96; private static final int SAMPLES = 96;
private static MantleDataAdapter<Matter> chunkAdapter;
private static final EnginePlatformHooks PLATFORM_HOOKS = new EnginePlatformHooks() {};
@BeforeClass @BeforeClass
public static void initializeMantleBlockState() throws Exception { public static void initializeMantleBlockState() throws Exception {
NativeBlockState air = mock(NativeBlockState.class); NativeBlockState air = mock(NativeBlockState.class, withSettings().stubOnly());
try (MockedStatic<B> blocks = mockStatic(B.class)) { try (MockedStatic<B> blocks = mockStatic(B.class)) {
blocks.when(() -> B.getState("AIR")).thenReturn(air); blocks.when(() -> B.getState("AIR")).thenReturn(air);
Class.forName(EngineMantle.class.getName()); Class.forName(EngineMantle.class.getName());
chunkAdapter = IrisEngineMantle.createRuntimeDataAdapter(null);
} }
} }
@@ -79,20 +83,18 @@ public class EngineBiomeColumnTest {
IrisBiome custom = biome("iris:custom_cave", 0); IrisBiome custom = biome("iris:custom_cave", 0);
IrisBiome configured = biome("iris:configured_cave", 0); IrisBiome configured = biome("iris:configured_cave", 0);
ProceduralStream<IrisBiome> surfaceStream = mock(ProceduralStream.class); Interpolated<IrisBiome> biomeInterpolation = Interpolated.of(biome -> 0D, value -> null);
when(surfaceStream.get(anyDouble(), anyDouble())).thenAnswer(call -> Interpolated<IrisRegion> regionInterpolation = Interpolated.of(region -> 0D, value -> null);
surfaces[pick(salt, call.getArgument(0), call.getArgument(1), 1, surfaces.length)]); ProceduralStream<IrisBiome> surfaceStream = ProceduralStream.of((x, z) ->
ProceduralStream<IrisBiome> caveStream = mock(ProceduralStream.class); surfaces[pick(salt, x, z, 1, surfaces.length)], biomeInterpolation);
when(caveStream.get(anyDouble(), anyDouble())).thenAnswer(call -> ProceduralStream<IrisBiome> caveStream = ProceduralStream.of((x, z) ->
caves[pick(salt, call.getArgument(0), call.getArgument(1), 2, caves.length)]); caves[pick(salt, x, z, 2, caves.length)], biomeInterpolation);
ProceduralStream<IrisRegion> regionStream = mock(ProceduralStream.class); ProceduralStream<IrisRegion> regionStream = ProceduralStream.of((x, z) ->
when(regionStream.get(anyDouble(), anyDouble())).thenAnswer(call -> regions[pick(salt, x, z, 3, regions.length)], regionInterpolation);
regions[pick(salt, call.getArgument(0), call.getArgument(1), 3, regions.length)]); ProceduralStream<Double> heightStream = ProceduralStream.of((x, z) ->
ProceduralStream<Double> heightStream = mock(ProceduralStream.class); (double) (40 + pick(salt, x, z, 4, 300)), Interpolated.DOUBLE);
when(heightStream.get(anyDouble(), anyDouble())).thenAnswer(call ->
(double) (40 + pick(salt, call.getArgument(0), call.getArgument(1), 4, 300)));
IrisComplex complex = mock(IrisComplex.class); IrisComplex complex = mock(IrisComplex.class, withSettings().stubOnly());
when(complex.getTrueBiomeStream()).thenReturn(surfaceStream); when(complex.getTrueBiomeStream()).thenReturn(surfaceStream);
when(complex.getCaveBiomeStream()).thenReturn(caveStream); when(complex.getCaveBiomeStream()).thenReturn(caveStream);
when(complex.getRegionStream()).thenReturn(regionStream); when(complex.getRegionStream()).thenReturn(regionStream);
@@ -100,27 +102,23 @@ public class EngineBiomeColumnTest {
when(complex.isTerrain3DSurface(anyInt(), anyInt(), anyInt())).thenAnswer(call -> 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); pick(salt, (int) call.getArgument(0) * 31 + (int) call.getArgument(1), (int) call.getArgument(2), 5, 9) == 0);
Mantle<Matter> mantle = mock(Mantle.class); Mantle<Matter> mantle = mock(Mantle.class, withSettings().stubOnly());
when(mantle.getWorldHeight()).thenReturn(384); when(mantle.getWorldHeight()).thenReturn(384);
when(mantle.getLoadedRegions()).thenReturn(new KMap<>()); when(mantle.getLoadedRegions()).thenReturn(new KMap<>());
when(mantle.hasTectonicPlate(anyInt(), anyInt())).thenReturn(true); when(mantle.hasTectonicPlate(anyInt(), anyInt())).thenReturn(true);
when(mantle.get(anyInt(), anyInt(), anyInt(), eq(MatterCavern.class))).thenAnswer(call -> when(mantle.get(anyInt(), anyInt(), anyInt(), eq(MatterCavern.class))).thenAnswer(call ->
cavern(salt, call.getArgument(0), call.getArgument(1), call.getArgument(2))); cavern(salt, call.getArgument(0), call.getArgument(1), call.getArgument(2)));
when(mantle.useChunk(anyInt(), anyInt())).thenAnswer(call -> { when(mantle.useChunk(anyInt(), anyInt())).thenAnswer(call ->
int chunkX = call.getArgument(0); new CoordinateChunk(salt, new Position2(call.getArgument(0), call.getArgument(1))).use());
int chunkZ = call.getArgument(1); EngineMantle engineMantle = (EngineMantle) Proxy.newProxyInstance(EngineMantle.class.getClassLoader(),
MantleChunk<Matter> chunk = mock(MantleChunk.class); new Class<?>[]{EngineMantle.class}, (proxy, method, arguments) -> switch (method.getName()) {
when(chunk.get(anyInt(), anyInt(), anyInt(), eq(MatterCavern.class))).thenAnswer(read -> cavern(salt, case "getMantle" -> mantle;
chunkX * 16 + (int) read.getArgument(0), read.getArgument(1), chunkZ * 16 + (int) read.getArgument(2))); case "getHighest" -> 20 + pick(salt, (int) arguments[0], (int) arguments[1], 7, 330);
return chunk; default -> throw new UnsupportedOperationException(method.getName());
}); });
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());
IrisData data = mock(IrisData.class); IrisData data = mock(IrisData.class, withSettings().stubOnly());
ResourceLoader<IrisBiome> loader = mock(ResourceLoader.class); ResourceLoader<IrisBiome> loader = mock(ResourceLoader.class, withSettings().stubOnly());
when(data.getBiomeLoader()).thenReturn(loader); when(data.getBiomeLoader()).thenReturn(loader);
when(loader.load(anyString())).thenReturn(null); when(loader.load(anyString())).thenReturn(null);
when(loader.load("iris:custom_cave")).thenReturn(custom); when(loader.load("iris:custom_cave")).thenReturn(custom);
@@ -136,15 +134,35 @@ public class EngineBiomeColumnTest {
dimension.setCarving(new KList<>(List.of(root))); dimension.setCarving(new KList<>(List.of(root)));
} }
EnginePlatformHooks hooks = mock(EnginePlatformHooks.class); IrisWorld world = IrisWorld.builder().minHeight(-64).maxHeight(320).build();
Engine engine = mock(Engine.class, CALLS_REAL_METHODS); return (Engine) Proxy.newProxyInstance(Engine.class.getClassLoader(), new Class<?>[]{Engine.class},
doReturn(complex).when(engine).getComplex(); (proxy, method, arguments) -> switch (method.getName()) {
doReturn(engineMantle).when(engine).getMantle(); case "getComplex" -> complex;
doReturn(hooks).when(engine).getPlatformHooks(); case "getMantle" -> engineMantle;
doReturn(data).when(engine).getData(); case "getPlatformHooks" -> PLATFORM_HOOKS;
doReturn(dimension).when(engine).getDimension(); case "getData" -> data;
doReturn(IrisWorld.builder().minHeight(-64).maxHeight(320).build()).when(engine).getWorld(); case "getDimension" -> dimension;
return engine; case "getWorld" -> world;
default -> InvocationHandler.invokeDefault(proxy, method, arguments);
});
}
private static final class CoordinateChunk extends MantleChunk<Matter> {
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> T get(int x, int y, int z, Class<T> 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) { private static MatterCavern cavern(long salt, int x, int y, int z) {
@@ -3,6 +3,8 @@ package art.arcane.iris.generation.stage;
import art.arcane.iris.testsupport.KeyedBlockState; import art.arcane.iris.testsupport.KeyedBlockState;
import art.arcane.iris.generation.runtime.Engine; 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.IrisDepositGenerator;
import art.arcane.iris.generation.decoration.IrisDepositHeightDistribution; import art.arcane.iris.generation.decoration.IrisDepositHeightDistribution;
import art.arcane.iris.generation.decoration.IrisDepositPlacementScope; 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.BurstExecutor;
import art.arcane.iris.generation.concurrent.MultiBurst; import art.arcane.iris.generation.concurrent.MultiBurst;
import art.arcane.iris.generation.context.ChunkContext; 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.hunk.Hunk;
import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk; 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.doReturn;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static org.mockito.Mockito.withSettings;
public class IrisDepositModifierOrderingTest { public class IrisDepositModifierOrderingTest {
@ClassRule @ClassRule
@@ -58,10 +64,10 @@ public class IrisDepositModifierOrderingTest {
@Before @Before
public void bindPlatform() { public void bindPlatform() {
IrisPlatforms.unbind(); IrisPlatforms.unbind();
PlatformRegistries registries = mock(PlatformRegistries.class); PlatformRegistries registries = mock(PlatformRegistries.class, withSettings().stubOnly());
NativeBlockState stone = state("stone"); NativeBlockState stone = state("stone");
when(registries.block(anyString())).thenReturn(stone); when(registries.block(anyString())).thenReturn(stone);
IrisPlatform platform = mock(IrisPlatform.class); IrisPlatform platform = mock(IrisPlatform.class, withSettings().stubOnly());
when(platform.registries()).thenReturn(registries); when(platform.registries()).thenReturn(registries);
IrisPlatforms.bind(platform); IrisPlatforms.bind(platform);
} }
@@ -274,8 +280,8 @@ public class IrisDepositModifierOrderingTest {
private final List<String> samples = Collections.synchronizedList(new ArrayList<>()); private final List<String> samples = Collections.synchronizedList(new ArrayList<>());
private final CountDownLatch biomeReady = new CountDownLatch(1); private final CountDownLatch biomeReady = new CountDownLatch(1);
private final CountDownLatch regionReady = new CountDownLatch(1); private final CountDownLatch regionReady = new CountDownLatch(1);
private final Engine engine = mock(Engine.class, RETURNS_DEEP_STUBS); private final Engine engine = mock(Engine.class, withSettings().stubOnly().defaultAnswer(RETURNS_DEEP_STUBS));
private final ChunkContext context = mock(ChunkContext.class, 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 stone = state("stone");
private final NativeBlockState granite = state("granite"); private final NativeBlockState granite = state("granite");
private final NativeBlockState diorite = state("diorite"); private final NativeBlockState diorite = state("diorite");
@@ -289,15 +295,23 @@ public class IrisDepositModifierOrderingTest {
private Fixture(boolean parallel, int attempts) { private Fixture(boolean parallel, int attempts) {
this.parallel = parallel; this.parallel = parallel;
this.attempts = attempts; this.attempts = attempts;
IrisBiome biome = mock(IrisBiome.class, withSettings().stubOnly());
IrisRegion region = mock(IrisRegion.class, withSettings().stubOnly());
ChunkedDataCache<IrisBiome> biomes = new ChunkedDataCache<>(
ProceduralStream.of((x, z) -> biome, Interpolated.of(value -> 0D, value -> null)), 0, 0);
ChunkedDataCache<IrisRegion> 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.getHeight()).thenReturn(16);
when(engine.getCaveBiome(anyInt(), anyInt(), anyInt(), any())).thenReturn(null); when(engine.getCaveBiome(anyInt(), anyInt(), anyInt(), any())).thenReturn(null);
when(engine.getDimension().getDepositVariants()).thenReturn(new KList<>()); 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); when(context.getGenerationSessionId()).thenReturn(11L);
chunk = mock(MantleChunk.class); chunk = mock(MantleChunk.class);
Mantle<Matter> mantle = engine.getMantle().getMantle(); Mantle<Matter> mantle = engine.getMantle().getMantle();
doReturn(chunk).when(mantle).useChunk(0, 0); 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); BurstExecutor burst = new BurstExecutor(executor, 3);
burst.setMulticore(parallel); burst.setMulticore(parallel);
when(engine.burst()).thenReturn(pool); when(engine.burst()).thenReturn(pool);
@@ -306,8 +320,8 @@ public class IrisDepositModifierOrderingTest {
IrisDepositGenerator regionDeposit = generator("region", granite, diorite); IrisDepositGenerator regionDeposit = generator("region", granite, diorite);
IrisDepositGenerator biomeDeposit = generator("biome", diorite, diamond); IrisDepositGenerator biomeDeposit = generator("biome", diorite, diamond);
when(engine.getDimension().getDeposits()).thenReturn(new KList<>(dimensionDeposit)); when(engine.getDimension().getDeposits()).thenReturn(new KList<>(dimensionDeposit));
when(context.getRegion().get(7, 7).getDeposits()).thenReturn(new KList<>(regionDeposit)); when(region.getDeposits()).thenReturn(new KList<>(regionDeposit));
when(context.getBiome().get(7, 7).getDeposits()).thenReturn(new KList<>(biomeDeposit)); when(biome.getDeposits()).thenReturn(new KList<>(biomeDeposit));
for (int x = 0; x < 16; x++) { for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) { for (int z = 0; z < 16; z++) {
output.set(x, 4, z, stone); output.set(x, 4, z, stone);
@@ -316,7 +330,7 @@ public class IrisDepositModifierOrderingTest {
} }
private IrisDepositGenerator generator(String name, NativeBlockState requiredHost, NativeBlockState replacement) { 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.getSpawnChance()).thenReturn(1D);
when(generator.getPerClumpSpawnChance()).thenReturn(1D); when(generator.getPerClumpSpawnChance()).thenReturn(1D);
when(generator.getMinPerChunk()).thenReturn(attempts); when(generator.getMinPerChunk()).thenReturn(attempts);
@@ -355,7 +369,7 @@ public class IrisDepositModifierOrderingTest {
} }
private void generate() { private void generate() {
try (MockedStatic<B> blocks = mockStatic(B.class, CALLS_REAL_METHODS)) { try (MockedStatic<B> blocks = mockStatic(B.class, withSettings().stubOnly().defaultAnswer(CALLS_REAL_METHODS))) {
blocks.when(() -> B.toDeepSlateOre(any(), any())).thenAnswer(invocation -> invocation.getArgument(1)); blocks.when(() -> B.toDeepSlateOre(any(), any())).thenAnswer(invocation -> invocation.getArgument(1));
new IrisDepositModifier(engine).generateDeposits(output, 0, 0, parallel, context); new IrisDepositModifier(engine).generateDeposits(output, 0, 0, parallel, context);
} }
@@ -21,6 +21,9 @@ import art.arcane.iris.testsupport.PlatformLeakGuard;
import art.arcane.iris.generation.block.B; import art.arcane.iris.generation.block.B;
import art.arcane.iris.generation.concurrent.MultiBurst; import art.arcane.iris.generation.concurrent.MultiBurst;
import art.arcane.iris.generation.context.ChunkContext; 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.hunk.Hunk;
import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.mantle.runtime.Mantle; 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.mock;
import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static org.mockito.Mockito.withSettings;
public class IrisDepositModifierParityTest { public class IrisDepositModifierParityTest {
@ClassRule @ClassRule
@@ -63,9 +67,9 @@ public class IrisDepositModifierParityTest {
@Before @Before
public void bindPlatform() { public void bindPlatform() {
IrisPlatforms.unbind(); IrisPlatforms.unbind();
PlatformRegistries registries = mock(PlatformRegistries.class); PlatformRegistries registries = mock(PlatformRegistries.class, withSettings().stubOnly());
when(registries.block(anyString())).thenAnswer(invocation -> state(invocation.getArgument(0))); 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); when(platform.registries()).thenReturn(registries);
IrisPlatforms.bind(platform); IrisPlatforms.bind(platform);
} }
@@ -125,8 +129,8 @@ public class IrisDepositModifierParityTest {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private long[] generateConfigured(long seed, boolean multicore, boolean caveVariant, boolean enumerablePack, private long[] generateConfigured(long seed, boolean multicore, boolean caveVariant, boolean enumerablePack,
AtomicInteger caveBiomeLookups, AtomicInteger unmatchableClumps) { AtomicInteger caveBiomeLookups, AtomicInteger unmatchableClumps) {
Engine engine = mock(Engine.class, RETURNS_DEEP_STUBS); Engine engine = mock(Engine.class, withSettings().stubOnly().defaultAnswer(RETURNS_DEEP_STUBS));
ChunkContext context = mock(ChunkContext.class, RETURNS_DEEP_STUBS); ChunkContext context = mock(ChunkContext.class, withSettings().stubOnly().defaultAnswer(RETURNS_DEEP_STUBS));
IrisDimension dimension = new IrisDimension(); IrisDimension dimension = new IrisDimension();
IrisRegion region = new IrisRegion(); IrisRegion region = new IrisRegion();
IrisBiome surface = new IrisBiome(); IrisBiome surface = new IrisBiome();
@@ -170,9 +174,9 @@ public class IrisDepositModifierParityTest {
when(engine.getSeedManager().getDeposit()).thenReturn(seed); when(engine.getSeedManager().getDeposit()).thenReturn(seed);
when(engine.getDimension()).thenReturn(dimension); when(engine.getDimension()).thenReturn(dimension);
if (enumerablePack) { if (enumerablePack) {
IrisData data = mock(IrisData.class); IrisData data = mock(IrisData.class, withSettings().stubOnly());
ResourceLoader<IrisBiome> biomes = mock(ResourceLoader.class); ResourceLoader<IrisBiome> biomes = mock(ResourceLoader.class, withSettings().stubOnly());
ResourceLoader<IrisRegion> regions = mock(ResourceLoader.class); ResourceLoader<IrisRegion> regions = mock(ResourceLoader.class, withSettings().stubOnly());
when(data.getBiomeLoader()).thenReturn(biomes); when(data.getBiomeLoader()).thenReturn(biomes);
when(data.getRegionLoader()).thenReturn(regions); when(data.getRegionLoader()).thenReturn(regions);
when(biomes.getPossibleKeys()).thenReturn(new String[]{"allowed", "denied", "surface"}); 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; 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.getRoundedHeight(anyInt(), anyInt())).thenReturn(40);
when(context.getBiome().get(anyInt(), anyInt())).thenReturn(surface); ChunkedDataCache<IrisBiome> biomes = new ChunkedDataCache<>(
when(context.getRegion().get(anyInt(), anyInt())).thenReturn(region); ProceduralStream.of((x, z) -> surface, Interpolated.of(value -> 0D, value -> null)), 0, 0);
MantleChunk<Matter> chunk = mock(MantleChunk.class); ChunkedDataCache<IrisRegion> 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<Matter> chunk = mock(MantleChunk.class, withSettings().stubOnly());
Mantle<Matter> mantle = engine.getMantle().getMantle(); Mantle<Matter> mantle = engine.getMantle().getMantle();
doReturn(chunk).when(mantle).useChunk(-3, 5); doReturn(chunk).when(mantle).useChunk(-3, 5);
MatterCavern cavern = new MatterCavern(true, "allowed", (byte) 0); MatterCavern cavern = new MatterCavern(true, "allowed", (byte) 0);
@@ -215,7 +223,7 @@ public class IrisDepositModifierParityTest {
} }
MultiBurst burst = new MultiBurst("Deposit parity", () -> 4); MultiBurst burst = new MultiBurst("Deposit parity", () -> 4);
when(engine.burst()).thenReturn(burst); when(engine.burst()).thenReturn(burst);
try (MockedStatic<B> blocks = mockStatic(B.class, CALLS_REAL_METHODS)) { try (MockedStatic<B> blocks = mockStatic(B.class, withSettings().stubOnly().defaultAnswer(CALLS_REAL_METHODS))) {
blocks.when(() -> B.getStateOrNull(anyString(), anyBoolean())) blocks.when(() -> B.getStateOrNull(anyString(), anyBoolean()))
.thenAnswer(invocation -> state(invocation.getArgument(0))); .thenAnswer(invocation -> state(invocation.getArgument(0)));
blocks.when(() -> B.toDeepSlateOre(any(), any())).thenAnswer(invocation -> { blocks.when(() -> B.toDeepSlateOre(any(), any())).thenAnswer(invocation -> {
@@ -30,7 +30,6 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame; import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any; 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.RETURNS_DEEP_STUBS;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@@ -245,27 +244,150 @@ public class IrisPerfectionModifierColumnTest {
if (existing != null) { if (existing != null) {
return existing; return existing;
} }
NativeBlockState block = mock(NativeBlockState.class, withSettings().stubOnly()); NativeBlockState block = new TestBlockState(this, key);
states.put(key, block); 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.<NativeBlockState>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; 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<NativeBlockState> { private static final class CountingHunk extends ArrayHunk<NativeBlockState> {
private int cleanColumnReads; private int cleanColumnReads;
@@ -18,6 +18,7 @@ import art.arcane.volmlib.util.collection.KList;
import org.junit.BeforeClass; import org.junit.BeforeClass;
import org.junit.Test; import org.junit.Test;
import org.mockito.invocation.InvocationOnMock; import org.mockito.invocation.InvocationOnMock;
import org.mockito.MockMakers;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
import java.lang.reflect.Field; 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.clearInvocations;
import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.withSettings;
import static org.mockito.Mockito.never; import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy; import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
@@ -348,7 +350,7 @@ public class IrisDimensionCarvingResolverParityTest {
Fixture replacement = createMixedDepthFixture(); Fixture replacement = createMixedDepthFixture();
AtomicReference<IrisDimension> dimension = new AtomicReference<>(first.engine.getDimension()); AtomicReference<IrisDimension> dimension = new AtomicReference<>(first.engine.getDimension());
AtomicReference<IrisData> data = new AtomicReference<>(first.engine.getData()); AtomicReference<IrisData> 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) -> dimension.get()).when(engine).getDimension();
doAnswer((InvocationOnMock invocation) -> data.get()).when(engine).getData(); doAnswer((InvocationOnMock invocation) -> data.get()).when(engine).getData();
@@ -601,7 +603,7 @@ public class IrisDimensionCarvingResolverParityTest {
doReturn(carvingEntries).when(dimension).getCarving(); doReturn(carvingEntries).when(dimension).getCarving();
doReturn(index).when(dimension).getCarvingEntryIndex(); 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(dimension).when(engine).getDimension();
doReturn(data).when(engine).getData(); doReturn(data).when(engine).getData();
doReturn(new SeedManager(913_531_771L)).when(engine).getSeedManager(); doReturn(new SeedManager(913_531_771L)).when(engine).getSeedManager();
@@ -689,7 +691,7 @@ public class IrisDimensionCarvingResolverParityTest {
doReturn(carvingEntries).when(dimension).getCarving(); doReturn(carvingEntries).when(dimension).getCarving();
doReturn(index).when(dimension).getCarvingEntryIndex(); 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(dimension).when(engine).getDimension();
doReturn(data).when(engine).getData(); doReturn(data).when(engine).getData();
doReturn(new SeedManager(4_627_991_643L)).when(engine).getSeedManager(); doReturn(new SeedManager(4_627_991_643L)).when(engine).getSeedManager();
@@ -2,6 +2,7 @@ package art.arcane.iris.studio.view;
import art.arcane.iris.configuration.IrisSettings; import art.arcane.iris.configuration.IrisSettings;
import art.arcane.iris.world.pregen.PregenApiPhase; 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.PregenApiSink;
import art.arcane.iris.world.pregen.PregenListener; import art.arcane.iris.world.pregen.PregenListener;
import art.arcane.iris.world.pregen.PregenTask; 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.iris.spi.IrisServices;
import art.arcane.volmlib.util.math.Position2; import art.arcane.volmlib.util.math.Position2;
import org.junit.Test; import org.junit.Test;
import org.mockito.MockedStatic;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; 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.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
public class PregeneratorJobTerminalProgressTest { public class PregeneratorJobTerminalProgressTest {
@@ -89,6 +93,7 @@ public class PregeneratorJobTerminalProgressTest {
PregeneratorMethod method = mock(PregeneratorMethod.class); PregeneratorMethod method = mock(PregeneratorMethod.class);
when(method.getMethod(anyInt(), anyInt())).thenReturn("terminal-progress-test"); when(method.getMethod(anyInt(), anyInt())).thenReturn("terminal-progress-test");
ArrayList<PregenListener> pending = new ArrayList<>(); ArrayList<PregenListener> pending = new ArrayList<>();
AtomicReference<MockedStatic<MantleHeapPressure>> heapPressure = new AtomicReference<>();
doAnswer(invocation -> { doAnswer(invocation -> {
pending.add(invocation.getArgument(2)); pending.add(invocation.getArgument(2));
if (completion.cancel()) { if (completion.cancel()) {
@@ -97,6 +102,7 @@ public class PregeneratorJobTerminalProgressTest {
return null; return null;
}).when(method).generateChunk(anyInt(), anyInt(), any(PregenListener.class)); }).when(method).generateChunk(anyInt(), anyInt(), any(PregenListener.class));
doAnswer(invocation -> { doAnswer(invocation -> {
try {
for (int index = 0; index < pending.size(); index++) { for (int index = 0; index < pending.size(); index++) {
PregenListener listener = pending.get(index); PregenListener listener = pending.get(index);
if (completion.failLast() && index == pending.size() - 1) { if (completion.failLast() && index == pending.size() - 1) {
@@ -105,6 +111,12 @@ public class PregeneratorJobTerminalProgressTest {
listener.onChunkGenerated(0, 0, completion.cached()); listener.onChunkGenerated(0, 0, completion.cached());
} }
} }
} finally {
MockedStatic<MantleHeapPressure> pressure = heapPressure.getAndSet(null);
if (pressure != null) {
pressure.close();
}
}
return null; return null;
}).when(method).close(); }).when(method).close();
PregenTask task = PregenTask.builder().center(new Position2(0, 0)) PregenTask task = PregenTask.builder().center(new Position2(0, 0))
@@ -112,7 +124,8 @@ public class PregeneratorJobTerminalProgressTest {
Thread worker = null; Thread worker = null;
try { try {
assertNull(PregeneratorJob.getInstance()); 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"); Field workerField = PregeneratorJob.class.getDeclaredField("worker");
workerField.setAccessible(true); workerField.setAccessible(true);
worker = (Thread) workerField.get(job); worker = (Thread) workerField.get(job);
@@ -1,6 +1,8 @@
package art.arcane.iris.world.history; package art.arcane.iris.world.history;
import art.arcane.iris.generation.runtime.Engine; 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.DimensionStackContext;
import art.arcane.iris.generation.runtime.DimensionStackLayout; import art.arcane.iris.generation.runtime.DimensionStackLayout;
import art.arcane.iris.generation.subterrain.SubterrainCell; import art.arcane.iris.generation.subterrain.SubterrainCell;
@@ -87,7 +89,7 @@ public class SavedBiomeCaptureTest {
@Test @Test
public void retainsFloatingChildIdentityWhenItSharesTheHostDerivative() throws Exception { public void retainsFloatingChildIdentityWhenItSharesTheHostDerivative() throws Exception {
Engine engine = mock(Engine.class, RETURNS_DEEP_STUBS); Engine engine = captureEngine();
when(engine.getComplex().getSubterrainPlanner()).thenReturn(null); when(engine.getComplex().getSubterrainPlanner()).thenReturn(null);
GenerationHistory.GenerationStage stage = mock(GenerationHistory.GenerationStage.class); GenerationHistory.GenerationStage stage = mock(GenerationHistory.GenerationStage.class);
SavedBiomeRuntime historical = mock(SavedBiomeRuntime.class); SavedBiomeRuntime historical = mock(SavedBiomeRuntime.class);
@@ -129,7 +131,7 @@ public class SavedBiomeCaptureTest {
@Test @Test
public void recordsExactSurfaceColumnsAndIndependentCaveAndVerticalIdentities() throws Exception { public void recordsExactSurfaceColumnsAndIndependentCaveAndVerticalIdentities() throws Exception {
Engine engine = mock(Engine.class, RETURNS_DEEP_STUBS); Engine engine = captureEngine();
when(engine.getComplex().getSubterrainPlanner()).thenReturn(null); when(engine.getComplex().getSubterrainPlanner()).thenReturn(null);
GenerationHistory.GenerationStage stage = mock(GenerationHistory.GenerationStage.class); GenerationHistory.GenerationStage stage = mock(GenerationHistory.GenerationStage.class);
SavedBiomeRuntime historical = mock(SavedBiomeRuntime.class); SavedBiomeRuntime historical = mock(SavedBiomeRuntime.class);
@@ -170,15 +172,15 @@ public class SavedBiomeCaptureTest {
@Test @Test
public void preservesHostFeatureBiomeAndRegionAgainstStackAndFloatingOverlay() throws Exception { public void preservesHostFeatureBiomeAndRegionAgainstStackAndFloatingOverlay() throws Exception {
Engine engine = mock(Engine.class, RETURNS_DEEP_STUBS); Engine engine = captureEngine();
when(engine.getComplex().getSubterrainPlanner()).thenReturn(null); when(engine.getComplex().getSubterrainPlanner()).thenReturn(null);
IrisBiome upper = biome("upper-biome"); IrisBiome upper = biome("upper-biome");
IrisBiome authored = biome("authored-biome"); IrisBiome authored = biome("authored-biome");
IrisRegion upperRegion = region("upper-region"); IrisRegion upperRegion = region("upper-region");
IrisRegion hostRegion = region("host-region"); IrisRegion hostRegion = region("host-region");
DimensionStackContext stack = mock(DimensionStackContext.class); DimensionStackContext stack = mock(DimensionStackContext.class, withSettings().stubOnly());
DimensionStackLayout layout = mock(DimensionStackLayout.class); DimensionStackLayout layout = mock(DimensionStackLayout.class, withSettings().stubOnly());
DimensionStackLayout.Layer layer = mock(DimensionStackLayout.Layer.class); DimensionStackLayout.Layer layer = mock(DimensionStackLayout.Layer.class, withSettings().stubOnly());
when(engine.getDimensionStackContext()).thenReturn(stack); when(engine.getDimensionStackContext()).thenReturn(stack);
when(stack.getLayout(anyInt(), anyInt())).thenReturn(layout); when(stack.getLayout(anyInt(), anyInt())).thenReturn(layout);
when(layout.layerAt(anyInt())).thenReturn(layer); when(layout.layerAt(anyInt())).thenReturn(layer);
@@ -220,15 +222,14 @@ public class SavedBiomeCaptureTest {
} }
int chunkX = Math.floorDiv(edgeX, 16); int chunkX = Math.floorDiv(edgeX, 16);
int chunkZ = Math.floorDiv(anchor.z(), 16); int chunkZ = Math.floorDiv(anchor.z(), 16);
Engine engine = mock(Engine.class, withSettings().defaultAnswer(RETURNS_DEEP_STUBS) Engine engine = captureEngine();
.mockMaker(MockMakers.SUBCLASS));
IrisBiome fallback = biome("base-biome"); IrisBiome fallback = biome("base-biome");
IrisBiome authored = biome("authored-biome"); IrisBiome authored = biome("authored-biome");
IrisRegion host = region("host-region"); IrisRegion host = region("host-region");
IrisRegion stacked = region("stacked-region"); IrisRegion stacked = region("stacked-region");
DimensionStackContext stack = mock(DimensionStackContext.class); DimensionStackContext stack = mock(DimensionStackContext.class, withSettings().stubOnly());
DimensionStackLayout layout = mock(DimensionStackLayout.class); DimensionStackLayout layout = mock(DimensionStackLayout.class, withSettings().stubOnly());
DimensionStackLayout.Layer layer = mock(DimensionStackLayout.Layer.class); DimensionStackLayout.Layer layer = mock(DimensionStackLayout.Layer.class, withSettings().stubOnly());
when(engine.getDimensionStackContext()).thenReturn(stack); when(engine.getDimensionStackContext()).thenReturn(stack);
when(stack.getLayout(anyInt(), anyInt())).thenReturn(layout); when(stack.getLayout(anyInt(), anyInt())).thenReturn(layout);
when(layout.layerAt(anyInt())).thenReturn(layer); when(layout.layerAt(anyInt())).thenReturn(layer);
@@ -272,8 +273,21 @@ public class SavedBiomeCaptureTest {
assertTrue(outsideOccupiedVolume); 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<IrisRegion> regions = mock(ProceduralStream.class, withSettings().stubOnly());
ProceduralStream<IrisBiome> 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) { 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); when(engine.getComplex().getSubterrainPlanner()).thenReturn(null);
IrisRegion region = region("flat-region"); IrisRegion region = region("flat-region");
when(engine.getMinHeight()).thenReturn(-64); when(engine.getMinHeight()).thenReturn(-64);
@@ -35,6 +35,7 @@ import java.util.concurrent.TimeoutException;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.concurrent.locks.LockSupport;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
@@ -589,8 +590,14 @@ public class SavedBiomeRuntimeTest {
} }
private static void awaitIdle(SavedBiomeRuntime runtime) { private static void awaitIdle(SavedBiomeRuntime runtime) {
Await.reached("the runtime to drain its pending queries", Duration.ofSeconds(5L), long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5L);
() -> runtime.pendingQueryCount() == 0); 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()); assertEquals(0, runtime.pendingQueryCount());
} }
@@ -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");
}
}
@@ -414,7 +414,7 @@ public final class GenerationOrderProbe {
? generateParallel(session.engine(), scheduled, configuration) ? generateParallel(session.engine(), scheduled, configuration)
: generateSequential(session.engine(), scheduled, configuration); : generateSequential(session.engine(), scheduled, configuration);
long generationNanos = System.nanoTime() - started; long generationNanos = System.nanoTime() - started;
List<Throwable> reports = RealPackProbeSupport.settleAndDrain(); List<Throwable> reports = RealPackProbeSupport.settleAndDrain(session.engine());
if (!reports.isEmpty()) { if (!reports.isEmpty()) {
RealPackProbeSupport.printReports(LOG_PREFIX, order + " generation reports", reports); RealPackProbeSupport.printReports(LOG_PREFIX, order + " generation reports", reports);
throw reportedFailure(order + " generation", reports); throw reportedFailure(order + " generation", reports);
@@ -3156,7 +3156,7 @@ public final class HydrologyPackProbe {
} }
hydrologySeed = engine.getSeedManager().getBodies(); hydrologySeed = engine.getSeedManager().getBodies();
engineReadyNanos = session.readyNanos(); engineReadyNanos = session.readyNanos();
List<Throwable> reports = RealPackProbeSupport.settleAndDrain(); List<Throwable> reports = RealPackProbeSupport.settleAndDrain(engine);
if (!reports.isEmpty()) { if (!reports.isEmpty()) {
RealPackProbeSupport.printReports(LOG_PREFIX, "seed " + seed + " planning reports", reports); RealPackProbeSupport.printReports(LOG_PREFIX, "seed " + seed + " planning reports", reports);
throw reportedFailure("Hydrology coverage scan", reports); throw reportedFailure("Hydrology coverage scan", reports);
@@ -4153,7 +4153,7 @@ public final class HydrologyPackProbe {
System.out.println(new GeneratedProcessResult( System.out.println(new GeneratedProcessResult(
witness.selector(), verification).machineLine()); witness.selector(), verification).machineLine());
} }
List<Throwable> reports = RealPackProbeSupport.settleAndDrain(); List<Throwable> reports = RealPackProbeSupport.settleAndDrain(engine);
if (!reports.isEmpty()) { if (!reports.isEmpty()) {
RealPackProbeSupport.printReports(LOG_PREFIX, "generated chunk reports", reports); RealPackProbeSupport.printReports(LOG_PREFIX, "generated chunk reports", reports);
throw reportedFailure("Hydrology generated chunk process", reports); throw reportedFailure("Hydrology generated chunk process", reports);
@@ -1,9 +1,11 @@
package art.arcane.iris.probe; package art.arcane.iris.probe;
import art.arcane.iris.pack.loading.IrisData; 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.PackValidationResult;
import art.arcane.iris.pack.PackValidator; import art.arcane.iris.pack.PackValidator;
import art.arcane.iris.generation.runtime.IrisEngine; 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.Engine;
import art.arcane.iris.generation.runtime.EngineAssignedComponent; import art.arcane.iris.generation.runtime.EngineAssignedComponent;
import art.arcane.iris.generation.runtime.EngineEffects; import art.arcane.iris.generation.runtime.EngineEffects;
@@ -39,6 +41,7 @@ import java.util.EnumSet;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.StandardCopyOption; import java.nio.file.StandardCopyOption;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
@@ -46,6 +49,13 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.ExecutorService; 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; import java.util.stream.Stream;
final class RealPackProbeSupport { final class RealPackProbeSupport {
@@ -79,6 +89,7 @@ final class RealPackProbeSupport {
validatePack(pack, logPrefix); validatePack(pack, logPrefix);
return new Workspace(workRoot, pack, dimensionKey, logPrefix, false, bindings); return new Workspace(workRoot, pack, dimensionKey, logPrefix, false, bindings);
} catch (Throwable failure) { } catch (Throwable failure) {
clearRuntime();
Throwable cleanupFailure = deleteWorkRoot(workRoot); Throwable cleanupFailure = deleteWorkRoot(workRoot);
if (cleanupFailure != null) { if (cleanupFailure != null) {
failure.addSuppressed(cleanupFailure); failure.addSuppressed(cleanupFailure);
@@ -103,6 +114,7 @@ final class RealPackProbeSupport {
configureRuntime(new File(workRoot, "platform-validation"), hydrologyGeneratedVerification, null); configureRuntime(new File(workRoot, "platform-validation"), hydrologyGeneratedVerification, null);
return new Workspace(workRoot, pack, dimensionKey, logPrefix, hydrologyGeneratedVerification, null); return new Workspace(workRoot, pack, dimensionKey, logPrefix, hydrologyGeneratedVerification, null);
} catch (Throwable failure) { } catch (Throwable failure) {
clearRuntime();
Throwable cleanupFailure = deleteWorkRoot(workRoot); Throwable cleanupFailure = deleteWorkRoot(workRoot);
if (cleanupFailure != null) { if (cleanupFailure != null) {
failure.addSuppressed(cleanupFailure); failure.addSuppressed(cleanupFailure);
@@ -120,7 +132,12 @@ final class RealPackProbeSupport {
} }
} }
static List<Throwable> settleAndDrain() throws InterruptedException { static List<Throwable> settleAndDrain(Engine engine) throws InterruptedException {
if (IrisPlatforms.isBound() && IrisPlatforms.get() instanceof StubPlatform) {
ProbeBackgroundTasks.await(engine);
awaitGenerationWorkers();
return drainReported();
}
List<Throwable> drained = new ArrayList<>(); List<Throwable> drained = new ArrayList<>();
long quietSince = System.currentTimeMillis(); long quietSince = System.currentTimeMillis();
long start = quietSince; long start = quietSince;
@@ -139,6 +156,54 @@ final class RealPackProbeSupport {
return drained; return drained;
} }
private static void awaitGenerationWorkers() throws InterruptedException {
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(15);
List<ForkJoinPool> pools = new ArrayList<>(3);
for (MultiBurst burst : List.of(MultiBurst.hydrology, MultiBurst.ioBurst, MultiBurst.burst)) {
try {
ForkJoinPool pool = burst.submit((Callable<ForkJoinPool>) 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<ForkJoinPool> 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<Throwable> reports) { static void printReports(String logPrefix, String label, List<Throwable> reports) {
if (reports.isEmpty()) { if (reports.isEmpty()) {
return; return;
@@ -305,7 +370,7 @@ final class RealPackProbeSupport {
router.preloadActiveRuntimes(); router.preloadActiveRuntimes();
} }
long readyNanos = System.nanoTime() - started; long readyNanos = System.nanoTime() - started;
List<Throwable> initializationReports = settleAndDrain(); List<Throwable> initializationReports = settleAndDrain(engine);
printReports(logPrefix, runLabel + " engine-init reports", initializationReports); printReports(logPrefix, runLabel + " engine-init reports", initializationReports);
if (bindings != null && !initializationReports.isEmpty()) { if (bindings != null && !initializationReports.isEmpty()) {
IllegalStateException failure = new IllegalStateException("Native engine initialization reported failures"); IllegalStateException failure = new IllegalStateException("Native engine initialization reported failures");
@@ -317,6 +382,7 @@ final class RealPackProbeSupport {
return new EngineSession(this, data, engine, readyNanos); return new EngineSession(this, data, engine, readyNanos);
} catch (Throwable failure) { } catch (Throwable failure) {
Throwable cleanupFailure = closeResources(engine, data); Throwable cleanupFailure = closeResources(engine, data);
clearRuntime();
sessionOpen = false; sessionOpen = false;
if (cleanupFailure != null) { if (cleanupFailure != null) {
failure.addSuppressed(cleanupFailure); failure.addSuppressed(cleanupFailure);
@@ -347,8 +413,7 @@ final class RealPackProbeSupport {
failure = bindingFailure; failure = bindingFailure;
} }
} }
IrisPlatforms.unbind(); clearRuntime();
StubPlatform.errorSink(null);
Throwable cleanupFailure = deleteWorkRoot(workRoot); Throwable cleanupFailure = deleteWorkRoot(workRoot);
if (cleanupFailure != null) { if (cleanupFailure != null) {
failure = appendFailure(failure, cleanupFailure); 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) { private static void configureRuntime(File platformRoot, boolean hydrologyGeneratedVerification, RuntimeBindings bindings) {
REPORTED.clear(); REPORTED.clear();
StubPlatform.bindGenerationStateHandlers(); StubPlatform.bindGenerationStateHandlers();
@@ -328,7 +328,7 @@ public final class Terrain3DProbe {
sections.addAll(renderer.write()); sections.addAll(renderer.write());
} }
topology = terrainTopology.inspect(); topology = terrainTopology.inspect();
failReported("generation", RealPackProbeSupport.settleAndDrain()); failReported("generation", RealPackProbeSupport.settleAndDrain(engine));
} }
List<String> failures = coverageFailures(evidence, configuration.requiredBiomes(), configuration.strictGeometry()); List<String> failures = coverageFailures(evidence, configuration.requiredBiomes(), configuration.strictGeometry());
Result result = new Result(failures.isEmpty() ? "PASS" : "FAIL", configuration, generatedChunks, Result result = new Result(failures.isEmpty() ? "PASS" : "FAIL", configuration, generatedChunks,
@@ -121,7 +121,7 @@ public final class TransitionProbe {
} }
require(historicalBiomes.equals(router.biomes().snapshot(0, 0).orElseThrow()), require(historicalBiomes.equals(router.biomes().snapshot(0, 0).orElseThrow()),
"Historical biome data changed after generation"); "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); throw new IllegalStateException("Generation reported a failure", failure);
} }
require(bindings.captures > 0, "Promotion did not capture historical terrain"); require(bindings.captures > 0, "Promotion did not capture historical terrain");
@@ -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<Thread> 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);
}
}
}
@@ -211,7 +211,7 @@ public final class ObjectContinuationProbeTest {
assertArrayEquals(oldReceipt, SavedTerrainChunk.readReceipt(world.worldFolder().toPath(), 0, 0)); assertArrayEquals(oldReceipt, SavedTerrainChunk.readReceipt(world.worldFolder().toPath(), 0, 0));
assertEquals(originalActivation, reopened.resolveActivation(0, 0).activationId()); assertEquals(originalActivation, reopened.resolveActivation(0, 0).activationId());
assertTrue(reopened.semantics(1, 0).orElseThrow().objectKeys().contains(fixture.objectKey())); 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 { } finally {
engine.close(); engine.close();
} }
@@ -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<Throwable> reports = RealPackProbeSupport.settleAndDrain(session.engine());
assertEquals(1, reports.size());
assertSame(failure, reports.getFirst());
assertTrue(RealPackProbeSupport.drainReported().isEmpty());
} finally {
release.countDown();
}
}
}