Relax test fsync

- Add Durability, a process-wide fsync policy defaulting to real barriers and switchable to relaxed through the iris.durability system property
- Route the history, studio, journal, structure and datapack fsync barriers through Durability so relaxed mode skips both the force and the channel open
- Add the DurabilityMode test rule and enable relaxed durability on the twenty slowest core stores tests
- Split GenerationHistoryTest into epoch, legacy adoption and recovery classes over a shared GenerationHistorySupport base
This commit is contained in:
Brian Neumann-Fopiano
2026-09-08 04:18:01 -04:00
parent c75cfd387e
commit c332c86849
34 changed files with 862 additions and 621 deletions
@@ -37,6 +37,7 @@ import art.arcane.iris.core.structure.authoring.StructureTransactionWriter;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisImportedStructureControl;
import art.arcane.iris.util.common.format.C;
import art.arcane.iris.util.common.io.Durability;
import art.arcane.iris.util.common.plugin.VolmitSender;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.io.IO;
@@ -5396,17 +5397,25 @@ public final class DatapackIngestService {
}
private static void forceFile(Path file) throws IOException {
if (!Durability.enabled()) {
return;
}
try (FileChannel channel = FileChannel.open(file, StandardOpenOption.WRITE)) {
channel.force(true);
Durability.force(channel);
}
}
private static void forceDirectoryIfSupported(Path directory) throws IOException {
if (!Durability.enabled()) {
return;
}
if (!Files.getFileStore(directory).supportsFileAttributeView("posix")) {
return;
}
try (FileChannel channel = FileChannel.open(directory, StandardOpenOption.READ)) {
channel.force(true);
Durability.force(channel);
}
}
@@ -2,6 +2,7 @@ package art.arcane.iris.core.lifecycle;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.util.common.io.Durability;
import java.io.File;
import java.io.IOException;
import java.nio.channels.FileChannel;
@@ -19,11 +20,15 @@ final class DirectoryDurability {
}
static void forceDirectoryRequired(Path directory) throws IOException {
if (!Durability.enabled()) {
return;
}
if (File.separatorChar == '\\') {
return;
}
try (FileChannel channel = FileChannel.open(directory, StandardOpenOption.READ)) {
channel.force(true);
Durability.force(channel);
} catch (UnsupportedOperationException failure) {
throw new IOException("Directory durability sync is unavailable for " + directory + ".", failure);
}
@@ -5,6 +5,7 @@ import art.arcane.iris.core.IrisWorldStorage;
import art.arcane.iris.core.WorldSlotKey;
import art.arcane.iris.core.lifecycle.BukkitWorldConfiguration.WorldGeneratorSnapshot;
import art.arcane.iris.util.common.io.Durability;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
@@ -345,7 +346,7 @@ public final class WorldReplacementJournal {
while (buffer.hasRemaining()) {
channel.write(buffer);
}
channel.force(true);
Durability.force(channel);
}
forceDirectoryRequired(parent);
try {
@@ -12,6 +12,7 @@ import art.arcane.iris.core.structure.authoring.StructureTransactionWriter;
import art.arcane.iris.core.structure.authoring.StructureWriteOptions;
import art.arcane.iris.core.structure.authoring.StructureWriteResult;
import art.arcane.iris.engine.framework.structure.StructureResourceBundleGraphCompiler;
import art.arcane.iris.util.common.io.Durability;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
@@ -303,7 +304,7 @@ final class JigsawStudioHistoryStore {
while (buffer.hasRemaining()) {
channel.write(buffer);
}
channel.force(true);
Durability.force(channel);
}
try {
Files.move(
@@ -374,6 +375,10 @@ final class JigsawStudioHistoryStore {
}
private static void forceDirectory(Path directory) throws IOException {
if (!Durability.enabled()) {
return;
}
// A directory can only be opened and fsynced on a POSIX filesystem; Windows rejects the
// open outright. Matches DirectoryDurability and DatapackIngestService, which already
// skip the barrier there.
@@ -381,7 +386,7 @@ final class JigsawStudioHistoryStore {
return;
}
try (FileChannel channel = FileChannel.open(directory, StandardOpenOption.READ)) {
channel.force(true);
Durability.force(channel);
}
}
@@ -18,6 +18,7 @@
package art.arcane.iris.core.structure.authoring;
import art.arcane.iris.util.common.io.Durability;
import java.io.IOException;
import java.io.InputStream;
import java.nio.channels.FileChannel;
@@ -70,17 +71,25 @@ interface StructureFileOperations {
}
default void forceFile(Path path) throws IOException {
if (!Durability.enabled()) {
return;
}
try (FileChannel channel = FileChannel.open(path, StandardOpenOption.WRITE)) {
channel.force(true);
Durability.force(channel);
}
}
default void forceDirectory(Path path) throws IOException {
if (!Durability.enabled()) {
return;
}
if (!Files.getFileStore(path).supportsFileAttributeView("posix")) {
return;
}
try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ)) {
channel.force(true);
Durability.force(channel);
}
}
}
@@ -1,6 +1,7 @@
package art.arcane.iris.engine.history;
import art.arcane.iris.core.pack.AtomicDirectoryPublisher;
import art.arcane.iris.util.common.io.Durability;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import java.io.File;
@@ -1153,11 +1154,15 @@ public final class GenerationHistory {
}
private static void forceDirectory(Path directory) throws IOException {
if (!Durability.enabled()) {
return;
}
if (File.separatorChar == '\\') {
return;
}
try (FileChannel channel = FileChannel.open(directory, StandardOpenOption.READ)) {
channel.force(true);
Durability.force(channel);
} catch (UnsupportedOperationException exception) {
throw new IOException("Generation history directory cannot be durability-synced: "
+ directory, exception);
@@ -1,5 +1,6 @@
package art.arcane.iris.engine.history;
import art.arcane.iris.util.common.io.Durability;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
@@ -282,7 +283,7 @@ public final class GenerationHistoryStore {
while (buffer.hasRemaining()) {
channel.write(buffer);
}
channel.force(true);
Durability.force(channel);
}
try {
if (replaceExisting) {
@@ -352,7 +353,7 @@ public final class GenerationHistoryStore {
while (buffer.hasRemaining()) {
channel.write(buffer);
}
channel.force(true);
Durability.force(channel);
}
try {
Files.move(temporary, metadata, StandardCopyOption.ATOMIC_MOVE);
@@ -439,11 +440,15 @@ public final class GenerationHistoryStore {
}
private static void forceDirectory(Path directory) throws IOException {
if (!Durability.enabled()) {
return;
}
if (File.separatorChar == '\\') {
return;
}
try (FileChannel channel = FileChannel.open(directory, StandardOpenOption.READ)) {
channel.force(true);
Durability.force(channel);
} catch (UnsupportedOperationException exception) {
throw new IOException("Generation history directory cannot be durability-synced: " + directory, exception);
}
@@ -1,6 +1,7 @@
package art.arcane.iris.engine.history;
import art.arcane.iris.engine.hydrology.HydrologyFeatureType;
import art.arcane.iris.util.common.io.Durability;
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
@@ -1949,14 +1950,14 @@ public final class GenerationSemanticIndex {
channel.write(entry);
}
}
channel.force(true);
Durability.force(channel);
if (created) {
RegionShard.forceDirectory(directory);
}
} catch (IOException failure) {
try {
channel.truncate(size);
channel.force(true);
Durability.force(channel);
if (created) {
RegionShard.forceDirectory(directory);
}
@@ -2079,7 +2080,7 @@ public final class GenerationSemanticIndex {
private static void truncateTail(RandomAccessFile file, long length) throws IOException {
file.setLength(length);
file.getFD().sync();
Durability.force(file.getFD());
}
private static IOException invalid(Path file, String reason) {
@@ -2951,16 +2952,20 @@ public final class GenerationSemanticIndex {
while (buffer.hasRemaining()) {
channel.write(buffer);
}
channel.force(true);
Durability.force(channel);
}
}
private static void forceDirectory(Path directory) throws IOException {
if (!Durability.enabled()) {
return;
}
if (File.separatorChar == '\\') {
return;
}
try (FileChannel channel = FileChannel.open(directory, StandardOpenOption.READ)) {
channel.force(true);
Durability.force(channel);
} catch (UnsupportedOperationException error) {
throw new IOException("Generation semantic directory cannot be durability-synced", error);
}
@@ -0,0 +1,33 @@
package art.arcane.iris.util.common.io;
import java.io.FileDescriptor;
import java.io.IOException;
import java.nio.channels.FileChannel;
public final class Durability {
public static final String MODE_PROPERTY = "iris.durability";
private static final String RELAXED = "relaxed";
private Durability() {
}
public static boolean enabled() {
return !RELAXED.equalsIgnoreCase(System.getProperty(MODE_PROPERTY));
}
public static void force(FileChannel channel) throws IOException {
if (!enabled()) {
return;
}
channel.force(true);
}
public static void force(FileDescriptor descriptor) throws IOException {
if (!enabled()) {
return;
}
descriptor.sync();
}
}
@@ -6,7 +6,9 @@ import art.arcane.iris.engine.history.GenerationEpochContractFactory;
import art.arcane.iris.engine.history.GenerationHistory;
import art.arcane.iris.engine.history.GenerationPackFingerprint;
import art.arcane.iris.engine.history.GenerationRegistryContract;
import art.arcane.iris.testsupport.DurabilityMode;
import org.junit.Assume;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
@@ -26,6 +28,9 @@ import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
public class IrisDatapackCompilerInputFingerprintTest {
@ClassRule
public static final DurabilityMode DURABILITY = DurabilityMode.relaxed();
@Rule
public TemporaryFolder tmp = new TemporaryFolder();
@@ -17,8 +17,10 @@ import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformGenerationRegistry;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.iris.testsupport.DurabilityMode;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.json.JSONObject;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.Assume;
@@ -42,6 +44,9 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class IrisDatapackCompilerTest {
@ClassRule
public static final DurabilityMode DURABILITY = DurabilityMode.relaxed();
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@@ -10,6 +10,7 @@ import art.arcane.iris.core.structure.authoring.StructureTransactionWriter;
import art.arcane.iris.core.structure.authoring.StructureWriteMode;
import art.arcane.iris.core.structure.authoring.StructureWriteResult;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.testsupport.DurabilityMode;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.io.IO;
import com.google.gson.Gson;
@@ -18,6 +19,7 @@ import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.sun.net.httpserver.HttpServer;
import org.bukkit.Server;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.Assume;
@@ -54,6 +56,9 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class DatapackIngestServiceTest {
@ClassRule
public static final DurabilityMode DURABILITY = DurabilityMode.relaxed();
private interface PaperLikeServer extends Server {
String getMinecraftVersion();
}
@@ -11,6 +11,7 @@ import art.arcane.iris.engine.history.GenerationEpochContractFactory;
import art.arcane.iris.engine.history.GenerationHistory;
import art.arcane.iris.engine.history.GenerationPackFingerprint;
import art.arcane.iris.engine.history.GenerationRegistryContract;
import art.arcane.iris.testsupport.DurabilityMode;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.ClassRule;
@@ -32,6 +33,9 @@ import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class WorldReplacementBootstrapTest {
@ClassRule
public static final DurabilityMode DURABILITY = DurabilityMode.relaxed();
private static final WorldSlotKey WORLD_KEY = WorldSlotKey.minecraft("the_nether");
private static final long SEED = 4242424242L;
@@ -5,6 +5,7 @@ import art.arcane.iris.engine.history.GenerationEpochContractFactory;
import art.arcane.iris.engine.history.GenerationHistory;
import art.arcane.iris.engine.history.GenerationPackFingerprint;
import art.arcane.iris.engine.history.GenerationRegistryContract;
import art.arcane.iris.testsupport.DurabilityMode;
import org.junit.Assume;
import org.junit.BeforeClass;
import org.junit.ClassRule;
@@ -30,6 +31,9 @@ import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class WorldReplacementFilesystemTest {
@ClassRule
public static final DurabilityMode DURABILITY = DurabilityMode.relaxed();
private static final UUID TRANSACTION_ID = UUID.fromString("00000000-0000-0000-0000-000000000001");
private static final UUID OTHER_TRANSACTION_ID = UUID.fromString("00000000-0000-0000-0000-000000000002");
@@ -3,9 +3,11 @@ package art.arcane.iris.core.pack;
import art.arcane.iris.core.lifecycle.BukkitStartupPaths;
import art.arcane.iris.engine.history.GenerationRegistryContractFactory;
import art.arcane.iris.engine.object.IrisBiomeCustom;
import art.arcane.iris.testsupport.DurabilityMode;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;
import org.junit.Assume;
import org.junit.ClassRule;
import org.junit.Test;
import java.io.ByteArrayOutputStream;
@@ -39,6 +41,9 @@ import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class DefaultPackBootstrapProvisionerTest {
@ClassRule
public static final DurabilityMode DURABILITY = DurabilityMode.relaxed();
@Test
public void startupDoesNotRequireOrDownloadDefaultPacks() {
assertTrue(DefaultPackBootstrapProvisioner.defaultPacks().isEmpty());
@@ -6,6 +6,7 @@ import art.arcane.iris.engine.object.IrisJigsawPiece;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.engine.object.IrisPosition;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.testsupport.DurabilityMode;
import art.arcane.iris.util.common.math.IrisBlockVector;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
@@ -36,6 +37,9 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class JigsawStudioGraphEditorResizeTest {
@ClassRule
public static final DurabilityMode DURABILITY = DurabilityMode.relaxed();
private static final String STRUCTURE_KEY = "resize/planar";
@ClassRule
@@ -10,6 +10,7 @@ import art.arcane.iris.core.structure.authoring.StructureWriteMode;
import art.arcane.iris.core.structure.authoring.StructureWriteResult;
import art.arcane.iris.engine.object.IrisJigsawThemeSet;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.testsupport.DurabilityMode;
import art.arcane.iris.util.common.math.IrisBlockVector;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
@@ -40,6 +41,9 @@ import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class JigsawStudioPersistenceEditorsTest {
@ClassRule
public static final DurabilityMode DURABILITY = DurabilityMode.relaxed();
private static final String PLANAR_KEY = "studio/planar";
private static final String SPATIAL_KEY = "studio/spatial";
@@ -9,6 +9,7 @@ import art.arcane.iris.engine.framework.structure.StructureGraphCompilation;
import art.arcane.iris.engine.framework.structure.StructureResourceBundleGraphCompiler;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.engine.object.IrisPosition;
import art.arcane.iris.testsupport.DurabilityMode;
import art.arcane.iris.util.common.math.IrisBlockVector;
import art.arcane.volmlib.util.math.RNG;
import com.google.gson.JsonObject;
@@ -35,6 +36,9 @@ import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
public class JigsawStudioProjectCreatorTest {
@ClassRule
public static final DurabilityMode DURABILITY = DurabilityMode.relaxed();
private static final JigsawStudioProjectCreator.Options DEFAULT_PLANAR_OPTIONS =
new JigsawStudioProjectCreator.Options(
"settlement/test",
@@ -11,6 +11,8 @@ import art.arcane.iris.core.structure.authoring.StructureTransactionWriter;
import art.arcane.iris.core.structure.authoring.StructureWriteOptions;
import art.arcane.iris.core.structure.authoring.StructureWriteResult;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.testsupport.DurabilityMode;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
@@ -29,6 +31,9 @@ import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class JigsawStudioHistoryStoreTest {
@ClassRule
public static final DurabilityMode DURABILITY = DurabilityMode.relaxed();
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@@ -17,6 +17,7 @@ import art.arcane.iris.engine.object.IrisJigsawPiece;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.engine.object.IrisPosition;
import art.arcane.iris.engine.object.JigsawJoint;
import art.arcane.iris.testsupport.DurabilityMode;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject;
@@ -46,6 +47,9 @@ import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class JigsawStudioResourceBundleAssemblerTest {
@ClassRule
public static final DurabilityMode DURABILITY = DurabilityMode.relaxed();
private static final Gson PRETTY_GSON = new GsonBuilder().setPrettyPrinting().create();
private static final String STRUCTURE_KEY = "fort";
@@ -18,6 +18,8 @@
package art.arcane.iris.core.structure.authoring;
import art.arcane.iris.testsupport.DurabilityMode;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.Assume;
@@ -40,6 +42,9 @@ import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class StructureTransactionWriterTest {
@ClassRule
public static final DurabilityMode DURABILITY = DurabilityMode.relaxed();
private static final StructureKey TARGET_KEY = StructureKey.parse("iris_test:temple");
private static final StructureKey SOURCE_KEY = StructureKey.parse("minecraft:trial_chambers");
@@ -26,8 +26,10 @@ import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.iris.testsupport.DurabilityMode;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.Rule;
import org.junit.rules.TemporaryFolder;
@@ -69,6 +71,9 @@ import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
public class EngineShutdownDrainTest {
@ClassRule
public static final DurabilityMode DURABILITY = DurabilityMode.relaxed();
private static IrisPlatform previousPlatform;
@Rule
@@ -1,6 +1,8 @@
package art.arcane.iris.engine.history;
import art.arcane.iris.testsupport.DurabilityMode;
import org.junit.Assume;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
@@ -19,6 +21,9 @@ import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public final class ChunkGenerationOwnershipTest {
@ClassRule
public static final DurabilityMode DURABILITY = DurabilityMode.relaxed();
@Rule
public final TemporaryFolder temporaryFolder = new TemporaryFolder();
@@ -0,0 +1,146 @@
package art.arcane.iris.engine.history;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class GenerationHistoryLegacyAdoptionTest extends GenerationHistorySupport {
@Test
public void legacyAdoptionIsDurableExplicitAndIdempotent() throws Exception {
Path world = temporaryFolder.newFolder("legacy-world").toPath();
Path legacy = Files.createDirectories(world.resolve("iris/pack/dimensions"));
Files.writeString(legacy.resolve("main.json"), "legacy");
Path legacyRoot = world.resolve("iris/pack");
Path legacyMantle = Files.createDirectories(world.resolve("mantle-hydrology"));
Files.writeString(legacyMantle.resolve("r.0.0.lz4b"), "mantle");
String fingerprint = fingerprint(legacyRoot);
GenerationHistory adopted = GenerationHistory.adoptLegacyPack(
world,
fingerprint,
42L,
contract(),
GenerationRegistryContract.empty()
);
assertFalse(Files.exists(legacyRoot));
assertFalse(Files.exists(legacyMantle));
assertTrue(Files.isDirectory(adopted.activePackRoot()));
Path activationMantle = adopted.paths().activationMantleRoot(1L);
assertEquals("mantle", Files.readString(activationMantle.resolve("r.0.0.lz4b")));
assertEquals(1L, adopted.activeActivation().activationId());
GenerationHistory repeated = GenerationHistory.adoptLegacyPack(
world,
fingerprint,
42L,
contract(),
GenerationRegistryContract.empty()
);
assertEquals(adopted.activeEpoch(), repeated.activeEpoch());
assertEquals("mantle", Files.readString(repeated.paths().activationMantleRoot(1L)
.resolve("r.0.0.lz4b")));
}
@Test
public void legacyMantleAdoptionFailsClosedOnDualRoots() throws Exception {
Path world = temporaryFolder.newFolder("legacy-dual-mantle-world").toPath();
Path legacyPack = Files.createDirectories(world.resolve("iris/pack/dimensions")).getParent();
Files.writeString(legacyPack.resolve("dimensions/main.json"), "legacy");
Path legacyMantle = Files.createDirectories(world.resolve("mantle-hydrology"));
Files.writeString(legacyMantle.resolve("legacy.bin"), "legacy");
GenerationHistoryPaths paths = GenerationHistoryPaths.forDimension(world);
Path activationMantle = Files.createDirectories(paths.activationMantleRoot(1L));
Files.writeString(activationMantle.resolve("activation.bin"), "activation");
IOException failure = assertThrows(
IOException.class,
() -> GenerationHistory.adoptLegacyPack(
world,
fingerprint(legacyPack),
42L,
contract(),
GenerationRegistryContract.empty()
)
);
assertTrue(failure.getMessage().contains("mantle roots conflict"));
assertEquals("legacy", Files.readString(legacyMantle.resolve("legacy.bin")));
assertEquals("activation", Files.readString(activationMantle.resolve("activation.bin")));
}
@Test
public void legacyMantleAdoptionRejectsRootAliases() throws Exception {
Path world = temporaryFolder.newFolder("legacy-aliased-mantle-world").toPath();
Path legacyPack = Files.createDirectories(world.resolve("iris/pack/dimensions")).getParent();
Files.writeString(legacyPack.resolve("dimensions/main.json"), "legacy");
GenerationHistoryPaths paths = GenerationHistoryPaths.forDimension(world);
Path activationMantle = Files.createDirectories(paths.activationMantleRoot(1L));
Path alias = paths.legacyMantleRoot();
try {
Files.createSymbolicLink(alias, activationMantle);
} catch (UnsupportedOperationException exception) {
return;
}
assertThrows(
IOException.class,
() -> GenerationHistory.adoptLegacyPack(
world,
fingerprint(legacyPack),
42L,
contract(),
GenerationRegistryContract.empty()
)
);
assertTrue(Files.isSymbolicLink(alias));
assertTrue(Files.isDirectory(activationMantle));
}
@Test
public void legacyAdoptionNeverDeletesMismatchedOrUnsafeInput() throws Exception {
Path mismatchWorld = temporaryFolder.newFolder("legacy-mismatch-world").toPath();
Path mismatchRoot = Files.createDirectories(mismatchWorld.resolve("iris/pack/dimensions")).getParent();
Files.writeString(mismatchRoot.resolve("dimensions/main.json"), "legacy");
assertThrows(
IOException.class,
() -> GenerationHistory.adoptLegacyPack(
mismatchWorld,
"f".repeat(64),
42L,
contract(),
GenerationRegistryContract.empty()
)
);
assertTrue(Files.isDirectory(mismatchRoot));
assertFalse(Files.exists(GenerationHistoryPaths.forDimension(mismatchWorld).manifest()));
Path linkedWorld = temporaryFolder.newFolder("legacy-linked-world").toPath();
Files.createDirectories(linkedWorld.resolve("iris"));
Path outside = temporaryFolder.newFolder("legacy-outside").toPath();
Path link = linkedWorld.resolve("iris/pack");
try {
Files.createSymbolicLink(link, outside);
} catch (UnsupportedOperationException exception) {
return;
}
assertThrows(
IOException.class,
() -> GenerationHistory.adoptLegacyPack(
linkedWorld,
"f".repeat(64),
42L,
contract(),
GenerationRegistryContract.empty()
)
);
assertTrue(Files.isDirectory(outside));
assertTrue(Files.isSymbolicLink(link));
}
}
@@ -0,0 +1,345 @@
package art.arcane.iris.engine.history;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Stream;
import org.junit.Test;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class GenerationHistoryRecoveryTest extends GenerationHistorySupport {
@Test
public void openFailsClosedOnDanglingOwnershipAndPartialHistory() throws Exception {
Path danglingWorld = temporaryFolder.newFolder("dangling-world").toPath();
Path pack = createPack("dangling-pack", "alpha");
GenerationHistory history = createHistory(danglingWorld, pack);
ChunkGenerationOwnership ownership = ChunkGenerationOwnership.load(history.paths().ownershipRoot());
ownership.assign(1, 2, 99L);
ownership.persist();
assertThrows(IOException.class, () -> GenerationHistory.open(danglingWorld));
Path absentWorld = temporaryFolder.newFolder("absent-world").toPath();
Optional<GenerationHistory> absent = GenerationHistory.openIfPresent(absentWorld);
assertTrue(absent.isEmpty());
Path partialWorld = temporaryFolder.newFolder("partial-world").toPath();
Files.createDirectories(GenerationHistoryPaths.forDimension(partialWorld).generationRoot());
assertThrows(NoSuchFileException.class, () -> GenerationHistory.openIfPresent(partialWorld));
}
@Test
public void savedHistoryOpensWithoutRetainingItsExecutableKernel() throws Exception {
Path world = temporaryFolder.newFolder("retained-abi-world").toPath();
Path packA = createPack("retained-abi-pack-a", "alpha");
Path packB = createPack("retained-abi-pack-b", "beta");
GenerationKernelRegistry.Version versionOne = new GenerationKernelRegistry.Version(1, 1, 1);
GenerationKernelRegistry.Version versionTwo = new GenerationKernelRegistry.Version(2, 1, 1);
GenerationKernelRegistry kernels = new GenerationKernelRegistry(
versionTwo,
List.of(
new GenerationKernelRegistry.Kernel(
1,
"1".repeat(64),
Map.of(
new GenerationKernelRegistry.AlgorithmVersion(1, 1),
(engine, transitionPlan) -> {
throw new AssertionError("History-only kernel factory was invoked.");
}
)
),
new GenerationKernelRegistry.Kernel(
2,
"2".repeat(64),
Map.of(
new GenerationKernelRegistry.AlgorithmVersion(1, 1),
(engine, transitionPlan) -> {
throw new AssertionError("History-only kernel factory was invoked.");
}
)
)
)
);
GenerationHistory history = GenerationHistory.create(
world,
packA,
fingerprint(packA),
42L,
contract(),
GenerationRegistryContract.empty(),
versionOne,
kernels
);
history.stageUpdate(
packB,
fingerprint(packB),
contract(),
GenerationRegistryContract.empty(),
256,
versionTwo
);
history.promotePending(List.of());
assertEquals(2, GenerationHistory.open(world).activeEpoch().generatorAbi());
GenerationHistory reopened = GenerationHistory.open(world, kernels);
assertEquals(2, reopened.activeEpoch().generatorAbi());
assertEquals(1, reopened.manifest().activation(1L).orElseThrow().activationId());
}
@Test
public void changedBuildExpandsFromSavedTerrainWithoutTheOldFactory() throws Exception {
Path world = temporaryFolder.newFolder("build-upgrade-world").toPath();
Path pack = createPack("build-upgrade-pack", "alpha");
GenerationKernelRegistry.Version oldVersion = new GenerationKernelRegistry.Version(101, 1, 1);
GenerationKernelRegistry.Version newVersion = new GenerationKernelRegistry.Version(102, 1, 1);
GenerationKernelRegistry firstBuild = singleKernel(oldVersion, "a");
GenerationKernelRegistry secondBuild = singleKernel(newVersion, "b");
GenerationHistory.create(world, pack, fingerprint(pack), 42L,
contract(), GenerationRegistryContract.empty(), oldVersion, firstBuild);
Path region = Files.createDirectories(world.resolve("region")).resolve("r.0.0.mca");
SavedTerrainTestRegion.write(region, new int[][]{{0, 0}});
byte[] savedBlocks = Files.readAllBytes(region);
GenerationHistory upgraded = GenerationHistory.open(world, secondBuild);
assertFalse(upgraded.usesCurrentGenerator());
upgraded.prepareCurrentGenerator(32);
assertTrue(upgraded.usesCurrentGenerator());
assertEquals(2L, upgraded.activeActivation().activationId());
assertEquals(oldVersion, upgraded.resolveEpoch(0, 0).kernelVersion());
try (GenerationHistory.GenerationStage generation = upgraded.openStage(1, 0)) {
assertEquals(newVersion, generation.epoch().kernelVersion());
}
assertTrue(upgraded.terrainSignatures(2L).size() > 0);
assertArrayEquals(savedBlocks, Files.readAllBytes(region));
GenerationHistory reopened = GenerationHistory.open(world, secondBuild);
assertTrue(reopened.usesCurrentGenerator());
assertEquals(2L, reopened.activeActivation().activationId());
}
@Test
public void openFailsClosedWhenAnOwnershipShardDisappears() throws Exception {
Path world = temporaryFolder.newFolder("missing-ownership-world").toPath();
Path packA = createPack("missing-ownership-pack-a", "alpha");
Path packB = createPack("missing-ownership-pack-b", "beta");
GenerationHistory history = createHistory(world, packA);
Path region = Files.createDirectories(world.resolve("region")).resolve("r.0.0.mca");
writeRegion(region, new int[][]{{1, 2}});
stage(history, packB);
history.promotePending(signaturesForChunks(new int[][]{{1, 2}}));
Files.delete(history.paths().ownershipRoot().resolve(RegionGenerationOwnership.fileName(0, 0)));
assertThrows(IOException.class, () -> GenerationHistory.open(world));
}
@Test
public void pendingCutoverResumesAfterPartialOutgoingOwnershipPublication() throws Exception {
Path world = temporaryFolder.newFolder("partial-cutover-world").toPath();
Path packA = createPack("partial-cutover-pack-a", "alpha");
Path packB = createPack("partial-cutover-pack-b", "beta");
GenerationHistory history = createHistory(world, packA);
Path region = Files.createDirectories(world.resolve("region")).resolve("r.0.0.mca");
int[][] chunks = {{1, 2}, {3, 4}};
writeRegion(region, chunks);
stage(history, packB);
ChunkGenerationOwnership partial = ChunkGenerationOwnership.load(history.paths().ownershipRoot());
assertTrue(partial.assign(1, 2, 1L));
partial.persist();
GenerationHistory recovered = GenerationHistory.open(world);
assertEquals(1, recovered.explicitChunkCount());
assertEquals(2L, recovered.promotePending(signaturesForChunks(chunks)).activationId());
assertEquals(2, GenerationHistory.open(world).explicitChunkCount());
}
@Test
public void pendingCutoverStillRejectsMissingOlderOwnership() throws Exception {
Path world = temporaryFolder.newFolder("pending-missing-old-world").toPath();
Path packA = createPack("pending-missing-old-pack-a", "alpha");
Path packB = createPack("pending-missing-old-pack-b", "beta");
Path packC = createPack("pending-missing-old-pack-c", "gamma");
GenerationHistory history = createHistory(world, packA);
Path region = Files.createDirectories(world.resolve("region")).resolve("r.0.0.mca");
writeRegion(region, new int[][]{{1, 2}});
stage(history, packB);
history.promotePending(signaturesForChunks(new int[][]{{1, 2}}));
stage(history, packC);
Files.delete(history.paths().ownershipRoot().resolve(RegionGenerationOwnership.fileName(0, 0)));
assertThrows(IOException.class, () -> GenerationHistory.open(world));
}
@Test
public void pendingCutoverDiscardsOutgoingOwnershipForUnstoredChunks() throws Exception {
Path world = temporaryFolder.newFolder("pending-forged-world").toPath();
Path packA = createPack("pending-forged-pack-a", "alpha");
Path packB = createPack("pending-forged-pack-b", "beta");
GenerationHistory history = createHistory(world, packA);
stage(history, packB);
ChunkGenerationOwnership forged = ChunkGenerationOwnership.load(history.paths().ownershipRoot());
assertTrue(forged.assign(9, 9, 1L));
forged.persist();
GenerationHistory recovered = GenerationHistory.open(world);
assertEquals(1, recovered.explicitChunkCount());
recovered.promotePending(List.of());
assertEquals(0, recovered.explicitChunkCount());
assertEquals(2L, recovered.resolveActivation(9, 9).activationId());
assertEquals(0, GenerationHistory.open(world).explicitChunkCount());
}
@Test
public void sealedGenerationClaimWithoutStoredTerrainIsDiscardedAfterCrash() throws Exception {
Path world = temporaryFolder.newFolder("semantic-claim-cutover-world").toPath();
Path packA = createPack("semantic-claim-cutover-pack-a", "alpha");
Path packB = createPack("semantic-claim-cutover-pack-b", "beta");
GenerationHistory history = createHistory(world, packA);
GenerationHistory.GenerationStage generation = history.openStage(9, 9);
ChunkGenerationSemantics claim = ChunkGenerationSemantics.builder(9, 9, 1L)
.addSurfaceBiome("iris:forest")
.seal()
.build();
assertTrue(history.claimGeneratedSemantics(generation, claim));
assertFalse(history.claimGeneratedSemantics(generation, claim));
generation.close();
assertThrows(IllegalStateException.class, () -> history.claimGeneratedSemantics(generation, claim));
assertFalse(Files.exists(world.resolve("region")));
stage(history, packB);
Path recoveredWorld = temporaryFolder.newFolder("restored-semantic-claim-world").toPath();
try (Stream<Path> entries = Files.walk(world)) {
for (Path source : entries.toList()) {
Path target = recoveredWorld.resolve(world.relativize(source));
if (Files.isDirectory(source)) {
Files.createDirectories(target);
} else {
Files.copy(source, target);
}
}
}
GenerationHistory recovered = GenerationHistory.open(recoveredWorld);
recovered.promotePending(List.of());
assertEquals(2L, recovered.resolveActivation(9, 9).activationId());
assertEquals(2L, recovered.resolveActivation(10, 9).activationId());
assertEquals(0, recovered.explicitChunkCount());
GenerationHistory reopened = GenerationHistory.open(recoveredWorld);
assertTrue(reopened.semantics(9, 9).isEmpty());
assertEquals(2L, reopened.resolveActivation(9, 9).activationId());
try (GenerationHistory.GenerationStage replacement = reopened.openStage(9, 9)) {
assertTrue(reopened.claimGeneratedSemantics(replacement,
ChunkGenerationSemantics.builder(9, 9, 2L).addObject("replacement").seal().build()));
}
}
@Test
public void coldRecoveryUsesNativeNoiseStatusAsTheMinimumStoredTerrain() throws Exception {
Path world = temporaryFolder.newFolder("native-status-recovery-world").toPath();
createHistory(world, createPack("native-status-recovery-pack", "alpha"));
Path regions = Files.createDirectories(world.resolve("region"));
SavedTerrainTestRegion.write(regions.resolve("r.0.0.mca"), new int[][]{{0, 0}}, "minecraft:noise");
SavedTerrainTestRegion.write(regions.resolve("r.1.0.mca"), new int[][]{{32, 0}}, "minecraft:biomes");
GenerationSemanticIndex index = GenerationSemanticIndex.loadRequired(world);
index.claimAndPersist(ChunkGenerationSemantics.builder(0, 0, 1L).addObject("saved-noise").seal().build());
index.claimAndPersist(ChunkGenerationSemantics.builder(32, 0, 1L).addObject("unsaved-terrain").seal().build());
GenerationHistory recovered = GenerationHistory.open(world);
recovered.prepareCurrentGenerator(64);
assertTrue(recovered.semantics(0, 0).isPresent());
assertTrue(recovered.semantics(32, 0).isEmpty());
assertEquals("minecraft:biomes", SavedTerrainChunk.readStatus(world, 32, 0));
assertEquals(0, recovered.explicitChunkCount());
GenerationHistory reopened = GenerationHistory.open(world);
assertTrue(reopened.semantics(0, 0).isPresent());
assertTrue(reopened.semantics(32, 0).isEmpty());
}
@Test
public void coldRecoveryKeepsStoredClaimsHistoricalMetadataAndFrozenPacks() throws Exception {
Path world = temporaryFolder.newFolder("cold-recovery-world").toPath();
Path packA = createPack("cold-recovery-a", "alpha");
Path packB = createPack("cold-recovery-b", "beta");
GenerationHistory initial = createHistory(world, packA);
String oldEpoch = initial.activeEpoch().epochId();
Path region = Files.createDirectories(world.resolve("region")).resolve("r.0.0.mca");
writeRegion(region, new int[][]{{0, 0}});
GenerationSemanticIndex initialIndex = GenerationSemanticIndex.loadRequired(world);
initialIndex.claimAndPersist(ChunkGenerationSemantics.builder(0, 0, 1L).addObject("historical").seal().build());
GenerationHistory first = GenerationHistory.open(world);
stage(first, packB);
first.promotePending(signaturesForChunks(new int[][]{{0, 0}}));
GenerationSemanticIndex index = GenerationSemanticIndex.loadRequired(world);
index.claimAndPersist(ChunkGenerationSemantics.builder(1, 0, 2L).addObject("saved").seal().build());
index.claimAndPersist(ChunkGenerationSemantics.builder(2, 0, 2L).addObject("orphan").seal().build());
writeRegion(region, new int[][]{{1, 0}});
GenerationHistory recovered = GenerationHistory.open(world);
recovered.prepareCurrentGenerator(64);
assertEquals(2L, recovered.activeActivation().activationId());
assertEquals(1, recovered.explicitChunkCount());
assertTrue(recovered.semantics(0, 0).isPresent());
assertTrue(recovered.semantics(1, 0).isPresent());
assertTrue(recovered.semantics(2, 0).isEmpty());
assertTrue(Files.isDirectory(recovered.paths().packRoot(oldEpoch)));
assertEquals(GenerationPackFingerprint.compute(packA, GenerationPackFingerprint.CURRENT_VERSION),
GenerationPackFingerprint.compute(recovered.paths().packRoot(oldEpoch), GenerationPackFingerprint.CURRENT_VERSION));
assertTrue(Files.isDirectory(recovered.activePackRoot()));
assertTrue(Files.isDirectory(packA));
GenerationHistory reopened = GenerationHistory.open(world);
assertEquals(1L, reopened.resolveActivation(0, 0).activationId());
assertTrue(reopened.semantics(2, 0).isEmpty());
}
@Test
public void openFailsClosedOnSemanticActivationMismatch() throws Exception {
Path world = temporaryFolder.newFolder("semantic-mismatch-world").toPath();
Path packA = createPack("semantic-mismatch-pack-a", "alpha");
Path packB = createPack("semantic-mismatch-pack-b", "beta");
GenerationHistory history = createHistory(world, packA);
stage(history, packB);
history.promotePending(List.of());
GenerationSemanticIndex index = GenerationSemanticIndex.loadRequired(world);
index.recordAndPersist(ChunkGenerationSemantics.builder(7, 8, 1L)
.addSurfaceBiome("iris:old")
.seal()
.build());
assertThrows(IOException.class, () -> GenerationHistory.open(world));
}
@Test
public void openFailsClosedOnDeletedOrCorruptTransitionSnapshots() throws Exception {
Path deletedWorld = temporaryFolder.newFolder("deleted-snapshot-world").toPath();
Path deletedPackA = createPack("deleted-snapshot-pack-a", "alpha");
Path deletedPackB = createPack("deleted-snapshot-pack-b", "beta");
GenerationHistory deletedHistory = createHistory(deletedWorld, deletedPackA);
stage(deletedHistory, deletedPackB);
deletedHistory.promotePending(List.of());
Files.delete(new TerrainBoundarySignatureStore(deletedWorld).snapshotPath(2L));
assertThrows(IOException.class, () -> GenerationHistory.open(deletedWorld));
Path corruptWorld = temporaryFolder.newFolder("corrupt-snapshot-world").toPath();
Path corruptPackA = createPack("corrupt-snapshot-pack-a", "alpha");
Path corruptPackB = createPack("corrupt-snapshot-pack-b", "beta");
GenerationHistory corruptHistory = createHistory(corruptWorld, corruptPackA);
stage(corruptHistory, corruptPackB);
corruptHistory.promotePending(List.of());
Files.write(new GenerationBoundaryStore(corruptWorld).snapshotPath(2L), new byte[]{1, 2, 3});
assertThrows(IOException.class, () -> GenerationHistory.open(corruptWorld));
}
}
@@ -9,10 +9,12 @@ import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.framework.GenerationSessionManager;
import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.testsupport.DurabilityMode;
import art.arcane.iris.util.common.data.B;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.matter.Matter;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.BeforeClass;
import org.junit.Test;
@@ -57,6 +59,9 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public final class GenerationHistoryRuntimeRouterTest {
@ClassRule
public static final DurabilityMode DURABILITY = DurabilityMode.relaxed();
@Rule
public final TemporaryFolder temporaryFolder = new TemporaryFolder();
@@ -1,8 +1,10 @@
package art.arcane.iris.engine.history;
import art.arcane.iris.testsupport.DurabilityMode;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import org.junit.Assume;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
@@ -31,6 +33,9 @@ import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class GenerationHistoryStoreTest {
@ClassRule
public static final DurabilityMode DURABILITY = DurabilityMode.relaxed();
private static final String PACK_A = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
private static final String PACK_B = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
private static final Clock CLOCK = Clock.fixed(Instant.ofEpochMilli(123456789L), ZoneOffset.UTC);
@@ -0,0 +1,145 @@
package art.arcane.iris.engine.history;
import art.arcane.iris.testsupport.DurabilityMode;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.rules.TemporaryFolder;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.Set;
import java.util.stream.Stream;
public abstract class GenerationHistorySupport {
@ClassRule
public static final DurabilityMode DURABILITY = DurabilityMode.relaxed();
@Rule
public final TemporaryFolder temporaryFolder = new TemporaryFolder();
static GenerationKernelRegistry singleKernel(GenerationKernelRegistry.Version version, String fingerprint) {
return new GenerationKernelRegistry(version, List.of(new GenerationKernelRegistry.Kernel(
version.generatorAbi(), fingerprint.repeat(64),
Map.of(new GenerationKernelRegistry.AlgorithmVersion(version.rngVersion(), version.seedDerivationVersion()),
(engine, transition) -> {
throw new AssertionError("Saved terrain capture invoked a generator factory");
}))));
}
void initializeRawHistory(
Path world,
Path pack,
int generatorAbi,
int rngVersion
) throws IOException {
String fingerprint = fingerprint(pack);
GenerationEpoch epoch = GenerationEpoch.create(new GenerationEpoch.Spec(
fingerprint,
GenerationPackFingerprint.CURRENT_VERSION,
42L,
GenerationEpoch.CURRENT_SEED_DERIVATION_VERSION,
generatorAbi,
rngVersion,
GenerationKernelV1.IMPLEMENTATION_FINGERPRINT,
contract(),
GenerationRegistryContract.empty()
));
GenerationPackRepository repository = new GenerationPackRepository(world);
repository.publish(
epoch.epochId(),
fingerprint,
GenerationPackFingerprint.CURRENT_VERSION,
pack
);
GenerationSemanticIndex.initialize(world);
GenerationHistoryStore.initialize(repository.generationRoot(), epoch);
}
Path createPack(String name, String content) throws IOException {
Path pack = temporaryFolder.newFolder(name).toPath();
Files.createDirectories(pack.resolve("dimensions"));
Files.writeString(pack.resolve("dimensions/main.json"), content);
return pack;
}
static String fingerprint(Path pack) throws IOException {
return GenerationPackFingerprint.compute(pack, GenerationPackFingerprint.CURRENT_VERSION);
}
static GenerationEpoch.DimensionContract upperContract(String packFingerprint) {
GenerationEpoch.DimensionContract base = contract();
return new GenerationEpoch.DimensionContract(
base.dimensionKey(), base.dimensionTypeKey(), base.environment(), base.generationMode(),
base.internalFluidHeight(), base.minHeight(), base.height(), base.logicalHeight(),
base.coordinateScale(), true, "ceiling", 32, packFingerprint,
base.dimensionTypeFingerprintSchema(), base.dimensionTypeFingerprint());
}
static GenerationEpoch.DimensionContract contract() {
return new GenerationEpoch.DimensionContract(
"overworld",
"iris:overworld_type",
"NORMAL",
"OVERWORLD",
127,
-64,
384,
384,
1D,
false,
"none",
0,
"0".repeat(64),
GenerationEpochContractFactory.CURRENT_DIMENSION_TYPE_FINGERPRINT_SCHEMA,
"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
);
}
static GenerationHistory createHistory(Path world, Path pack) throws IOException {
return GenerationHistory.create(
world,
pack,
fingerprint(pack),
42L,
contract(),
GenerationRegistryContract.empty()
);
}
static GenerationActivation stage(GenerationHistory history, Path pack) throws IOException {
return history.stageUpdate(
pack,
fingerprint(pack),
contract(),
GenerationRegistryContract.empty(),
256
);
}
static List<TerrainBoundarySignature> signaturesForChunks(int[][] chunks) {
ArrayList<GenerationBoundary.ChunkCoordinate> coordinates = new ArrayList<>(chunks.length);
for (int[] chunk : chunks) {
coordinates.add(new GenerationBoundary.ChunkCoordinate(chunk[0], chunk[1]));
}
GenerationBoundary boundary = GenerationBoundary.freeze("test-boundary", coordinates);
ArrayList<TerrainBoundarySignature> signatures = new ArrayList<>(boundary.exposedBlockColumns().size());
for (GenerationBoundary.BlockColumn column : boundary.exposedBlockColumns()) {
signatures.add(signature(column.blockX(), column.blockZ()));
}
return List.copyOf(signatures);
}
static TerrainBoundarySignature signature(int blockX, int blockZ) {
return new TerrainBoundarySignature(
new TerrainBoundarySignature.Column(blockX, blockZ, 64, 63, OptionalInt.empty(), OptionalInt.empty()),
new TerrainBoundarySignature.Samples(
new TerrainBoundarySignature.VerticalLayout(-64, 64, 2),
new TerrainBoundarySignature.BiomeEncoding(
List.of("iris:test"),
new short[]{0, 0}
)
)
, BoundaryColumnGeometry.empty());
}
static void writeRegion(Path file, int[][] chunks) throws IOException {
SavedTerrainTestRegion.write(file, chunks);
}
}
@@ -1,31 +1,17 @@
package art.arcane.iris.engine.history;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.Set;
import java.util.stream.Stream;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public final class GenerationHistoryTest {
@Rule
public final TemporaryFolder temporaryFolder = new TemporaryFolder();
public class GenerationHistoryTest extends GenerationHistorySupport {
@Test
public void upperContentUpdatesPreserveLayoutAndDistinctActivationsAcrossRestarts() throws Exception {
Path world = temporaryFolder.newFolder("upper-update-world").toPath();
@@ -235,159 +221,6 @@ public final class GenerationHistoryTest {
);
}
@Test
public void legacyAdoptionIsDurableExplicitAndIdempotent() throws Exception {
Path world = temporaryFolder.newFolder("legacy-world").toPath();
Path legacy = Files.createDirectories(world.resolve("iris/pack/dimensions"));
Files.writeString(legacy.resolve("main.json"), "legacy");
Path legacyRoot = world.resolve("iris/pack");
Path legacyMantle = Files.createDirectories(world.resolve("mantle-hydrology"));
Files.writeString(legacyMantle.resolve("r.0.0.lz4b"), "mantle");
String fingerprint = fingerprint(legacyRoot);
GenerationHistory adopted = GenerationHistory.adoptLegacyPack(
world,
fingerprint,
42L,
contract(),
GenerationRegistryContract.empty()
);
assertFalse(Files.exists(legacyRoot));
assertFalse(Files.exists(legacyMantle));
assertTrue(Files.isDirectory(adopted.activePackRoot()));
Path activationMantle = adopted.paths().activationMantleRoot(1L);
assertEquals("mantle", Files.readString(activationMantle.resolve("r.0.0.lz4b")));
assertEquals(1L, adopted.activeActivation().activationId());
GenerationHistory repeated = GenerationHistory.adoptLegacyPack(
world,
fingerprint,
42L,
contract(),
GenerationRegistryContract.empty()
);
assertEquals(adopted.activeEpoch(), repeated.activeEpoch());
assertEquals("mantle", Files.readString(repeated.paths().activationMantleRoot(1L)
.resolve("r.0.0.lz4b")));
}
@Test
public void legacyMantleAdoptionFailsClosedOnDualRoots() throws Exception {
Path world = temporaryFolder.newFolder("legacy-dual-mantle-world").toPath();
Path legacyPack = Files.createDirectories(world.resolve("iris/pack/dimensions")).getParent();
Files.writeString(legacyPack.resolve("dimensions/main.json"), "legacy");
Path legacyMantle = Files.createDirectories(world.resolve("mantle-hydrology"));
Files.writeString(legacyMantle.resolve("legacy.bin"), "legacy");
GenerationHistoryPaths paths = GenerationHistoryPaths.forDimension(world);
Path activationMantle = Files.createDirectories(paths.activationMantleRoot(1L));
Files.writeString(activationMantle.resolve("activation.bin"), "activation");
IOException failure = assertThrows(
IOException.class,
() -> GenerationHistory.adoptLegacyPack(
world,
fingerprint(legacyPack),
42L,
contract(),
GenerationRegistryContract.empty()
)
);
assertTrue(failure.getMessage().contains("mantle roots conflict"));
assertEquals("legacy", Files.readString(legacyMantle.resolve("legacy.bin")));
assertEquals("activation", Files.readString(activationMantle.resolve("activation.bin")));
}
@Test
public void legacyMantleAdoptionRejectsRootAliases() throws Exception {
Path world = temporaryFolder.newFolder("legacy-aliased-mantle-world").toPath();
Path legacyPack = Files.createDirectories(world.resolve("iris/pack/dimensions")).getParent();
Files.writeString(legacyPack.resolve("dimensions/main.json"), "legacy");
GenerationHistoryPaths paths = GenerationHistoryPaths.forDimension(world);
Path activationMantle = Files.createDirectories(paths.activationMantleRoot(1L));
Path alias = paths.legacyMantleRoot();
try {
Files.createSymbolicLink(alias, activationMantle);
} catch (UnsupportedOperationException exception) {
return;
}
assertThrows(
IOException.class,
() -> GenerationHistory.adoptLegacyPack(
world,
fingerprint(legacyPack),
42L,
contract(),
GenerationRegistryContract.empty()
)
);
assertTrue(Files.isSymbolicLink(alias));
assertTrue(Files.isDirectory(activationMantle));
}
@Test
public void legacyAdoptionNeverDeletesMismatchedOrUnsafeInput() throws Exception {
Path mismatchWorld = temporaryFolder.newFolder("legacy-mismatch-world").toPath();
Path mismatchRoot = Files.createDirectories(mismatchWorld.resolve("iris/pack/dimensions")).getParent();
Files.writeString(mismatchRoot.resolve("dimensions/main.json"), "legacy");
assertThrows(
IOException.class,
() -> GenerationHistory.adoptLegacyPack(
mismatchWorld,
"f".repeat(64),
42L,
contract(),
GenerationRegistryContract.empty()
)
);
assertTrue(Files.isDirectory(mismatchRoot));
assertFalse(Files.exists(GenerationHistoryPaths.forDimension(mismatchWorld).manifest()));
Path linkedWorld = temporaryFolder.newFolder("legacy-linked-world").toPath();
Files.createDirectories(linkedWorld.resolve("iris"));
Path outside = temporaryFolder.newFolder("legacy-outside").toPath();
Path link = linkedWorld.resolve("iris/pack");
try {
Files.createSymbolicLink(link, outside);
} catch (UnsupportedOperationException exception) {
return;
}
assertThrows(
IOException.class,
() -> GenerationHistory.adoptLegacyPack(
linkedWorld,
"f".repeat(64),
42L,
contract(),
GenerationRegistryContract.empty()
)
);
assertTrue(Files.isDirectory(outside));
assertTrue(Files.isSymbolicLink(link));
}
@Test
public void openFailsClosedOnDanglingOwnershipAndPartialHistory() throws Exception {
Path danglingWorld = temporaryFolder.newFolder("dangling-world").toPath();
Path pack = createPack("dangling-pack", "alpha");
GenerationHistory history = createHistory(danglingWorld, pack);
ChunkGenerationOwnership ownership = ChunkGenerationOwnership.load(history.paths().ownershipRoot());
ownership.assign(1, 2, 99L);
ownership.persist();
assertThrows(IOException.class, () -> GenerationHistory.open(danglingWorld));
Path absentWorld = temporaryFolder.newFolder("absent-world").toPath();
Optional<GenerationHistory> absent = GenerationHistory.openIfPresent(absentWorld);
assertTrue(absent.isEmpty());
Path partialWorld = temporaryFolder.newFolder("partial-world").toPath();
Files.createDirectories(GenerationHistoryPaths.forDimension(partialWorld).generationRoot());
assertThrows(NoSuchFileException.class, () -> GenerationHistory.openIfPresent(partialWorld));
}
@Test
public void stagingTheActiveEpochIsANoOp() throws Exception {
Path world = temporaryFolder.newFolder("same-epoch-world").toPath();
@@ -420,104 +253,6 @@ public final class GenerationHistoryTest {
assertEquals(3, history.manifest().activations().size());
}
@Test
public void savedHistoryOpensWithoutRetainingItsExecutableKernel() throws Exception {
Path world = temporaryFolder.newFolder("retained-abi-world").toPath();
Path packA = createPack("retained-abi-pack-a", "alpha");
Path packB = createPack("retained-abi-pack-b", "beta");
GenerationKernelRegistry.Version versionOne = new GenerationKernelRegistry.Version(1, 1, 1);
GenerationKernelRegistry.Version versionTwo = new GenerationKernelRegistry.Version(2, 1, 1);
GenerationKernelRegistry kernels = new GenerationKernelRegistry(
versionTwo,
List.of(
new GenerationKernelRegistry.Kernel(
1,
"1".repeat(64),
Map.of(
new GenerationKernelRegistry.AlgorithmVersion(1, 1),
(engine, transitionPlan) -> {
throw new AssertionError("History-only kernel factory was invoked.");
}
)
),
new GenerationKernelRegistry.Kernel(
2,
"2".repeat(64),
Map.of(
new GenerationKernelRegistry.AlgorithmVersion(1, 1),
(engine, transitionPlan) -> {
throw new AssertionError("History-only kernel factory was invoked.");
}
)
)
)
);
GenerationHistory history = GenerationHistory.create(
world,
packA,
fingerprint(packA),
42L,
contract(),
GenerationRegistryContract.empty(),
versionOne,
kernels
);
history.stageUpdate(
packB,
fingerprint(packB),
contract(),
GenerationRegistryContract.empty(),
256,
versionTwo
);
history.promotePending(List.of());
assertEquals(2, GenerationHistory.open(world).activeEpoch().generatorAbi());
GenerationHistory reopened = GenerationHistory.open(world, kernels);
assertEquals(2, reopened.activeEpoch().generatorAbi());
assertEquals(1, reopened.manifest().activation(1L).orElseThrow().activationId());
}
@Test
public void changedBuildExpandsFromSavedTerrainWithoutTheOldFactory() throws Exception {
Path world = temporaryFolder.newFolder("build-upgrade-world").toPath();
Path pack = createPack("build-upgrade-pack", "alpha");
GenerationKernelRegistry.Version oldVersion = new GenerationKernelRegistry.Version(101, 1, 1);
GenerationKernelRegistry.Version newVersion = new GenerationKernelRegistry.Version(102, 1, 1);
GenerationKernelRegistry firstBuild = singleKernel(oldVersion, "a");
GenerationKernelRegistry secondBuild = singleKernel(newVersion, "b");
GenerationHistory.create(world, pack, fingerprint(pack), 42L,
contract(), GenerationRegistryContract.empty(), oldVersion, firstBuild);
Path region = Files.createDirectories(world.resolve("region")).resolve("r.0.0.mca");
SavedTerrainTestRegion.write(region, new int[][]{{0, 0}});
byte[] savedBlocks = Files.readAllBytes(region);
GenerationHistory upgraded = GenerationHistory.open(world, secondBuild);
assertFalse(upgraded.usesCurrentGenerator());
upgraded.prepareCurrentGenerator(32);
assertTrue(upgraded.usesCurrentGenerator());
assertEquals(2L, upgraded.activeActivation().activationId());
assertEquals(oldVersion, upgraded.resolveEpoch(0, 0).kernelVersion());
try (GenerationHistory.GenerationStage generation = upgraded.openStage(1, 0)) {
assertEquals(newVersion, generation.epoch().kernelVersion());
}
assertTrue(upgraded.terrainSignatures(2L).size() > 0);
assertArrayEquals(savedBlocks, Files.readAllBytes(region));
GenerationHistory reopened = GenerationHistory.open(world, secondBuild);
assertTrue(reopened.usesCurrentGenerator());
assertEquals(2L, reopened.activeActivation().activationId());
}
private static GenerationKernelRegistry singleKernel(GenerationKernelRegistry.Version version, String fingerprint) {
return new GenerationKernelRegistry(version, List.of(new GenerationKernelRegistry.Kernel(
version.generatorAbi(), fingerprint.repeat(64),
Map.of(new GenerationKernelRegistry.AlgorithmVersion(version.rngVersion(), version.seedDerivationVersion()),
(engine, transition) -> {
throw new AssertionError("Saved terrain capture invoked a generator factory");
}))));
}
@Test
public void openRejectsWorldSeedMismatch() throws Exception {
Path world = temporaryFolder.newFolder("seed-world").toPath();
@@ -528,200 +263,6 @@ public final class GenerationHistoryTest {
assertEquals(42L, GenerationHistory.open(world, 42L).activeEpoch().worldSeed());
}
@Test
public void openFailsClosedWhenAnOwnershipShardDisappears() throws Exception {
Path world = temporaryFolder.newFolder("missing-ownership-world").toPath();
Path packA = createPack("missing-ownership-pack-a", "alpha");
Path packB = createPack("missing-ownership-pack-b", "beta");
GenerationHistory history = createHistory(world, packA);
Path region = Files.createDirectories(world.resolve("region")).resolve("r.0.0.mca");
writeRegion(region, new int[][]{{1, 2}});
stage(history, packB);
history.promotePending(signaturesForChunks(new int[][]{{1, 2}}));
Files.delete(history.paths().ownershipRoot().resolve(RegionGenerationOwnership.fileName(0, 0)));
assertThrows(IOException.class, () -> GenerationHistory.open(world));
}
@Test
public void pendingCutoverResumesAfterPartialOutgoingOwnershipPublication() throws Exception {
Path world = temporaryFolder.newFolder("partial-cutover-world").toPath();
Path packA = createPack("partial-cutover-pack-a", "alpha");
Path packB = createPack("partial-cutover-pack-b", "beta");
GenerationHistory history = createHistory(world, packA);
Path region = Files.createDirectories(world.resolve("region")).resolve("r.0.0.mca");
int[][] chunks = {{1, 2}, {3, 4}};
writeRegion(region, chunks);
stage(history, packB);
ChunkGenerationOwnership partial = ChunkGenerationOwnership.load(history.paths().ownershipRoot());
assertTrue(partial.assign(1, 2, 1L));
partial.persist();
GenerationHistory recovered = GenerationHistory.open(world);
assertEquals(1, recovered.explicitChunkCount());
assertEquals(2L, recovered.promotePending(signaturesForChunks(chunks)).activationId());
assertEquals(2, GenerationHistory.open(world).explicitChunkCount());
}
@Test
public void pendingCutoverStillRejectsMissingOlderOwnership() throws Exception {
Path world = temporaryFolder.newFolder("pending-missing-old-world").toPath();
Path packA = createPack("pending-missing-old-pack-a", "alpha");
Path packB = createPack("pending-missing-old-pack-b", "beta");
Path packC = createPack("pending-missing-old-pack-c", "gamma");
GenerationHistory history = createHistory(world, packA);
Path region = Files.createDirectories(world.resolve("region")).resolve("r.0.0.mca");
writeRegion(region, new int[][]{{1, 2}});
stage(history, packB);
history.promotePending(signaturesForChunks(new int[][]{{1, 2}}));
stage(history, packC);
Files.delete(history.paths().ownershipRoot().resolve(RegionGenerationOwnership.fileName(0, 0)));
assertThrows(IOException.class, () -> GenerationHistory.open(world));
}
@Test
public void pendingCutoverDiscardsOutgoingOwnershipForUnstoredChunks() throws Exception {
Path world = temporaryFolder.newFolder("pending-forged-world").toPath();
Path packA = createPack("pending-forged-pack-a", "alpha");
Path packB = createPack("pending-forged-pack-b", "beta");
GenerationHistory history = createHistory(world, packA);
stage(history, packB);
ChunkGenerationOwnership forged = ChunkGenerationOwnership.load(history.paths().ownershipRoot());
assertTrue(forged.assign(9, 9, 1L));
forged.persist();
GenerationHistory recovered = GenerationHistory.open(world);
assertEquals(1, recovered.explicitChunkCount());
recovered.promotePending(List.of());
assertEquals(0, recovered.explicitChunkCount());
assertEquals(2L, recovered.resolveActivation(9, 9).activationId());
assertEquals(0, GenerationHistory.open(world).explicitChunkCount());
}
@Test
public void sealedGenerationClaimWithoutStoredTerrainIsDiscardedAfterCrash() throws Exception {
Path world = temporaryFolder.newFolder("semantic-claim-cutover-world").toPath();
Path packA = createPack("semantic-claim-cutover-pack-a", "alpha");
Path packB = createPack("semantic-claim-cutover-pack-b", "beta");
GenerationHistory history = createHistory(world, packA);
GenerationHistory.GenerationStage generation = history.openStage(9, 9);
ChunkGenerationSemantics claim = ChunkGenerationSemantics.builder(9, 9, 1L)
.addSurfaceBiome("iris:forest")
.seal()
.build();
assertTrue(history.claimGeneratedSemantics(generation, claim));
assertFalse(history.claimGeneratedSemantics(generation, claim));
generation.close();
assertThrows(IllegalStateException.class, () -> history.claimGeneratedSemantics(generation, claim));
assertFalse(Files.exists(world.resolve("region")));
stage(history, packB);
Path recoveredWorld = temporaryFolder.newFolder("restored-semantic-claim-world").toPath();
try (Stream<Path> entries = Files.walk(world)) {
for (Path source : entries.toList()) {
Path target = recoveredWorld.resolve(world.relativize(source));
if (Files.isDirectory(source)) {
Files.createDirectories(target);
} else {
Files.copy(source, target);
}
}
}
GenerationHistory recovered = GenerationHistory.open(recoveredWorld);
recovered.promotePending(List.of());
assertEquals(2L, recovered.resolveActivation(9, 9).activationId());
assertEquals(2L, recovered.resolveActivation(10, 9).activationId());
assertEquals(0, recovered.explicitChunkCount());
GenerationHistory reopened = GenerationHistory.open(recoveredWorld);
assertTrue(reopened.semantics(9, 9).isEmpty());
assertEquals(2L, reopened.resolveActivation(9, 9).activationId());
try (GenerationHistory.GenerationStage replacement = reopened.openStage(9, 9)) {
assertTrue(reopened.claimGeneratedSemantics(replacement,
ChunkGenerationSemantics.builder(9, 9, 2L).addObject("replacement").seal().build()));
}
}
@Test
public void coldRecoveryUsesNativeNoiseStatusAsTheMinimumStoredTerrain() throws Exception {
Path world = temporaryFolder.newFolder("native-status-recovery-world").toPath();
createHistory(world, createPack("native-status-recovery-pack", "alpha"));
Path regions = Files.createDirectories(world.resolve("region"));
SavedTerrainTestRegion.write(regions.resolve("r.0.0.mca"), new int[][]{{0, 0}}, "minecraft:noise");
SavedTerrainTestRegion.write(regions.resolve("r.1.0.mca"), new int[][]{{32, 0}}, "minecraft:biomes");
GenerationSemanticIndex index = GenerationSemanticIndex.loadRequired(world);
index.claimAndPersist(ChunkGenerationSemantics.builder(0, 0, 1L).addObject("saved-noise").seal().build());
index.claimAndPersist(ChunkGenerationSemantics.builder(32, 0, 1L).addObject("unsaved-terrain").seal().build());
GenerationHistory recovered = GenerationHistory.open(world);
recovered.prepareCurrentGenerator(64);
assertTrue(recovered.semantics(0, 0).isPresent());
assertTrue(recovered.semantics(32, 0).isEmpty());
assertEquals("minecraft:biomes", SavedTerrainChunk.readStatus(world, 32, 0));
assertEquals(0, recovered.explicitChunkCount());
GenerationHistory reopened = GenerationHistory.open(world);
assertTrue(reopened.semantics(0, 0).isPresent());
assertTrue(reopened.semantics(32, 0).isEmpty());
}
@Test
public void coldRecoveryKeepsStoredClaimsHistoricalMetadataAndFrozenPacks() throws Exception {
Path world = temporaryFolder.newFolder("cold-recovery-world").toPath();
Path packA = createPack("cold-recovery-a", "alpha");
Path packB = createPack("cold-recovery-b", "beta");
GenerationHistory initial = createHistory(world, packA);
String oldEpoch = initial.activeEpoch().epochId();
Path region = Files.createDirectories(world.resolve("region")).resolve("r.0.0.mca");
writeRegion(region, new int[][]{{0, 0}});
GenerationSemanticIndex initialIndex = GenerationSemanticIndex.loadRequired(world);
initialIndex.claimAndPersist(ChunkGenerationSemantics.builder(0, 0, 1L).addObject("historical").seal().build());
GenerationHistory first = GenerationHistory.open(world);
stage(first, packB);
first.promotePending(signaturesForChunks(new int[][]{{0, 0}}));
GenerationSemanticIndex index = GenerationSemanticIndex.loadRequired(world);
index.claimAndPersist(ChunkGenerationSemantics.builder(1, 0, 2L).addObject("saved").seal().build());
index.claimAndPersist(ChunkGenerationSemantics.builder(2, 0, 2L).addObject("orphan").seal().build());
writeRegion(region, new int[][]{{1, 0}});
GenerationHistory recovered = GenerationHistory.open(world);
recovered.prepareCurrentGenerator(64);
assertEquals(2L, recovered.activeActivation().activationId());
assertEquals(1, recovered.explicitChunkCount());
assertTrue(recovered.semantics(0, 0).isPresent());
assertTrue(recovered.semantics(1, 0).isPresent());
assertTrue(recovered.semantics(2, 0).isEmpty());
assertTrue(Files.isDirectory(recovered.paths().packRoot(oldEpoch)));
assertEquals(GenerationPackFingerprint.compute(packA, GenerationPackFingerprint.CURRENT_VERSION),
GenerationPackFingerprint.compute(recovered.paths().packRoot(oldEpoch), GenerationPackFingerprint.CURRENT_VERSION));
assertTrue(Files.isDirectory(recovered.activePackRoot()));
assertTrue(Files.isDirectory(packA));
GenerationHistory reopened = GenerationHistory.open(world);
assertEquals(1L, reopened.resolveActivation(0, 0).activationId());
assertTrue(reopened.semantics(2, 0).isEmpty());
}
@Test
public void openFailsClosedOnSemanticActivationMismatch() throws Exception {
Path world = temporaryFolder.newFolder("semantic-mismatch-world").toPath();
Path packA = createPack("semantic-mismatch-pack-a", "alpha");
Path packB = createPack("semantic-mismatch-pack-b", "beta");
GenerationHistory history = createHistory(world, packA);
stage(history, packB);
history.promotePending(List.of());
GenerationSemanticIndex index = GenerationSemanticIndex.loadRequired(world);
index.recordAndPersist(ChunkGenerationSemantics.builder(7, 8, 1L)
.addSurfaceBiome("iris:old")
.seal()
.build());
assertThrows(IOException.class, () -> GenerationHistory.open(world));
}
@Test
public void promotionIsRejectedAfterGenerationAdmissionOpens() throws Exception {
Path world = temporaryFolder.newFolder("admission-world").toPath();
@@ -739,147 +280,4 @@ public final class GenerationHistoryTest {
generationStage.close();
assertThrows(IllegalStateException.class, () -> history.promotePending(List.of()));
}
@Test
public void openFailsClosedOnDeletedOrCorruptTransitionSnapshots() throws Exception {
Path deletedWorld = temporaryFolder.newFolder("deleted-snapshot-world").toPath();
Path deletedPackA = createPack("deleted-snapshot-pack-a", "alpha");
Path deletedPackB = createPack("deleted-snapshot-pack-b", "beta");
GenerationHistory deletedHistory = createHistory(deletedWorld, deletedPackA);
stage(deletedHistory, deletedPackB);
deletedHistory.promotePending(List.of());
Files.delete(new TerrainBoundarySignatureStore(deletedWorld).snapshotPath(2L));
assertThrows(IOException.class, () -> GenerationHistory.open(deletedWorld));
Path corruptWorld = temporaryFolder.newFolder("corrupt-snapshot-world").toPath();
Path corruptPackA = createPack("corrupt-snapshot-pack-a", "alpha");
Path corruptPackB = createPack("corrupt-snapshot-pack-b", "beta");
GenerationHistory corruptHistory = createHistory(corruptWorld, corruptPackA);
stage(corruptHistory, corruptPackB);
corruptHistory.promotePending(List.of());
Files.write(new GenerationBoundaryStore(corruptWorld).snapshotPath(2L), new byte[]{1, 2, 3});
assertThrows(IOException.class, () -> GenerationHistory.open(corruptWorld));
}
private void initializeRawHistory(
Path world,
Path pack,
int generatorAbi,
int rngVersion
) throws IOException {
String fingerprint = fingerprint(pack);
GenerationEpoch epoch = GenerationEpoch.create(new GenerationEpoch.Spec(
fingerprint,
GenerationPackFingerprint.CURRENT_VERSION,
42L,
GenerationEpoch.CURRENT_SEED_DERIVATION_VERSION,
generatorAbi,
rngVersion,
GenerationKernelV1.IMPLEMENTATION_FINGERPRINT,
contract(),
GenerationRegistryContract.empty()
));
GenerationPackRepository repository = new GenerationPackRepository(world);
repository.publish(
epoch.epochId(),
fingerprint,
GenerationPackFingerprint.CURRENT_VERSION,
pack
);
GenerationSemanticIndex.initialize(world);
GenerationHistoryStore.initialize(repository.generationRoot(), epoch);
}
private Path createPack(String name, String content) throws IOException {
Path pack = temporaryFolder.newFolder(name).toPath();
Files.createDirectories(pack.resolve("dimensions"));
Files.writeString(pack.resolve("dimensions/main.json"), content);
return pack;
}
private static String fingerprint(Path pack) throws IOException {
return GenerationPackFingerprint.compute(pack, GenerationPackFingerprint.CURRENT_VERSION);
}
private static GenerationEpoch.DimensionContract upperContract(String packFingerprint) {
GenerationEpoch.DimensionContract base = contract();
return new GenerationEpoch.DimensionContract(
base.dimensionKey(), base.dimensionTypeKey(), base.environment(), base.generationMode(),
base.internalFluidHeight(), base.minHeight(), base.height(), base.logicalHeight(),
base.coordinateScale(), true, "ceiling", 32, packFingerprint,
base.dimensionTypeFingerprintSchema(), base.dimensionTypeFingerprint());
}
private static GenerationEpoch.DimensionContract contract() {
return new GenerationEpoch.DimensionContract(
"overworld",
"iris:overworld_type",
"NORMAL",
"OVERWORLD",
127,
-64,
384,
384,
1D,
false,
"none",
0,
"0".repeat(64),
GenerationEpochContractFactory.CURRENT_DIMENSION_TYPE_FINGERPRINT_SCHEMA,
"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
);
}
private static GenerationHistory createHistory(Path world, Path pack) throws IOException {
return GenerationHistory.create(
world,
pack,
fingerprint(pack),
42L,
contract(),
GenerationRegistryContract.empty()
);
}
private static GenerationActivation stage(GenerationHistory history, Path pack) throws IOException {
return history.stageUpdate(
pack,
fingerprint(pack),
contract(),
GenerationRegistryContract.empty(),
256
);
}
private static List<TerrainBoundarySignature> signaturesForChunks(int[][] chunks) {
ArrayList<GenerationBoundary.ChunkCoordinate> coordinates = new ArrayList<>(chunks.length);
for (int[] chunk : chunks) {
coordinates.add(new GenerationBoundary.ChunkCoordinate(chunk[0], chunk[1]));
}
GenerationBoundary boundary = GenerationBoundary.freeze("test-boundary", coordinates);
ArrayList<TerrainBoundarySignature> signatures = new ArrayList<>(boundary.exposedBlockColumns().size());
for (GenerationBoundary.BlockColumn column : boundary.exposedBlockColumns()) {
signatures.add(signature(column.blockX(), column.blockZ()));
}
return List.copyOf(signatures);
}
private static TerrainBoundarySignature signature(int blockX, int blockZ) {
return new TerrainBoundarySignature(
new TerrainBoundarySignature.Column(blockX, blockZ, 64, 63, OptionalInt.empty(), OptionalInt.empty()),
new TerrainBoundarySignature.Samples(
new TerrainBoundarySignature.VerticalLayout(-64, 64, 2),
new TerrainBoundarySignature.BiomeEncoding(
List.of("iris:test"),
new short[]{0, 0}
)
)
, BoundaryColumnGeometry.empty());
}
private static void writeRegion(Path file, int[][] chunks) throws IOException {
SavedTerrainTestRegion.write(file, chunks);
}
}
@@ -1,7 +1,9 @@
package art.arcane.iris.engine.history;
import art.arcane.iris.engine.hydrology.HydrologyFeatureType;
import art.arcane.iris.testsupport.DurabilityMode;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
@@ -26,6 +28,9 @@ import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public final class GenerationSemanticIndexTest {
@ClassRule
public static final DurabilityMode DURABILITY = DurabilityMode.relaxed();
@Rule
public final TemporaryFolder temporaryFolder = new TemporaryFolder();
@@ -1,5 +1,7 @@
package art.arcane.iris.engine.history;
import art.arcane.iris.testsupport.DurabilityMode;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
@@ -20,6 +22,9 @@ import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class TransitionGenerationPlanTest {
@ClassRule
public static final DurabilityMode DURABILITY = DurabilityMode.relaxed();
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@@ -1,6 +1,8 @@
package art.arcane.iris.engine.history;
import art.arcane.iris.testsupport.DurabilityMode;
import art.arcane.iris.util.nbt.common.mca.MCAFile;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
@@ -22,6 +24,9 @@ import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public final class WorldChunkInventoryTest {
@ClassRule
public static final DurabilityMode DURABILITY = DurabilityMode.relaxed();
private static final int SECTOR_BYTES = 4_096;
@Rule
@@ -0,0 +1,41 @@
package art.arcane.iris.testsupport;
import art.arcane.iris.util.common.io.Durability;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
public final class DurabilityMode implements TestRule {
private static final String RELAXED = "relaxed";
private final String mode;
private DurabilityMode(String mode) {
this.mode = mode;
}
public static DurabilityMode relaxed() {
return new DurabilityMode(RELAXED);
}
@Override
public Statement apply(Statement base, Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
String previous = System.getProperty(Durability.MODE_PROPERTY);
System.setProperty(Durability.MODE_PROPERTY, mode);
try {
base.evaluate();
} finally {
if (previous == null) {
System.clearProperty(Durability.MODE_PROPERTY);
} else {
System.setProperty(Durability.MODE_PROPERTY, previous);
}
}
}
};
}
}