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