mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-20 07:24:16 +00:00
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:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+11
-2
@@ -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());
|
||||
|
||||
+4
@@ -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
|
||||
|
||||
+4
@@ -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";
|
||||
|
||||
|
||||
+4
@@ -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();
|
||||
|
||||
|
||||
+4
@@ -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";
|
||||
|
||||
|
||||
+5
@@ -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();
|
||||
|
||||
|
||||
+146
@@ -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));
|
||||
}
|
||||
}
|
||||
+5
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user