This commit is contained in:
Brian Neumann-Fopiano committed 2026-08-20 10:59:17 -04:00
1 parent fec655def4
commit 5cacf6d435
62 files changed
+4892 -277

No files matched your search

@@ -0,0 +1,194 @@
package art.arcane.iris;
import art.arcane.iris.core.lifecycle.BukkitStartupPaths;
import art.arcane.iris.core.lifecycle.MissingWorldStorageLog;
import org.bukkit.configuration.file.YamlConfiguration;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
/**
* The server enumerates levels from disk and CraftBukkit falls back to the vanilla generator whenever the
* plugin cannot supply one, so a world with storage Iris cannot use has to stop startup before any level is
* created. A world with no storage at all is only reported.
*/
public class IrisBootstrapWorldStorageContractTest {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Before
public void clearOrphanReports() {
MissingWorldStorageLog.reset();
}
@Test
public void startupIsRefusedWhenAConfiguredWorldLostItsPackSnapshot() throws Exception {
Path serverRoot = temporaryFolder.newFolder("broken-server").toPath();
Files.createDirectories(serverRoot.resolve("world/dimensions/iris/orphan2/region"));
Files.writeString(serverRoot.resolve("world/dimensions/iris/orphan2/region/r.0.0.mca"), "terrain");
writeServerProperties(serverRoot);
writeBukkitWorlds(serverRoot, "world_iris_orphan2");
IllegalStateException failure = assertThrows(
IllegalStateException.class,
() -> IrisBootstrap.requireUsableWorldStorage(startupPaths(serverRoot)));
assertTrue(failure.getMessage(), failure.getMessage().contains("world_iris_orphan2"));
assertTrue(failure.getMessage(),
failure.getMessage().contains(serverRoot.toRealPath().resolve("world/dimensions/iris/orphan2")
.toString()));
}
/**
* Deleting a live world's folder and letting the server save it back leaves a data/ skeleton with no
* regions and no iris/ directory. It owns nothing, so refusing to boot over it only strands the server.
*/
@Test
public void startupContinuesForAServerRewrittenHuskAndReportsItOnce() throws Exception {
Path serverRoot = temporaryFolder.newFolder("husk-server").toPath();
Files.createDirectories(serverRoot.resolve("world/dimensions/iris/husk/data/paper"));
Files.writeString(serverRoot.resolve("world/dimensions/iris/husk/data/paper/level_overrides.dat"), "x");
writeServerProperties(serverRoot);
writeBukkitWorlds(serverRoot, "world_iris_husk");
IrisBootstrap.requireUsableWorldStorage(startupPaths(serverRoot));
assertTrue(MissingWorldStorageLog.hasWarned("world_iris_husk"));
}
/**
* Both classifications of a hot-deleted world's husk stop the boot: UNUSABLE trips Iris' fail-closed
* guard, and EMPTY excludes it from the dimension registry, which trips Paper's interactive world
* migration gate. The husk has to leave the dimensions tree before either can happen.
*/
@Test
public void aHotDeletedWorldsHuskIsMovedOutOfTheDimensionsTree() throws Exception {
Path serverRoot = temporaryFolder.newFolder("husk-quarantine").toPath();
Files.createDirectories(serverRoot.resolve("world/dimensions/iris/husk/data/paper"));
Files.writeString(serverRoot.resolve("world/dimensions/iris/husk/data/paper/level_overrides.dat"), "x");
writeServerProperties(serverRoot);
writeBukkitWorlds(serverRoot, "world_iris_husk");
List<String> warnings = new ArrayList<>();
IrisBootstrap.quarantineWorthlessHusks(startupPaths(serverRoot), warnings::add);
IrisBootstrap.requireUsableWorldStorage(startupPaths(serverRoot));
assertFalse(Files.exists(serverRoot.resolve("world/dimensions/iris/husk")));
assertEquals(1, warnings.size());
assertTrue(warnings.getFirst(),
warnings.getFirst().contains("/iris remove world=world_iris_husk delete=true"));
assertTrue(MissingWorldStorageLog.hasWarned("world_iris_husk"));
}
/**
* Paper enumerates the dimensions tree from disk, so a husk whose bukkit.yml entry is already gone stops
* the boot exactly the same way. The sweep is over the tree, not over the configuration.
*/
@Test
public void aHuskWithNoBukkitConfigurationEntryIsStillMovedOut() throws Exception {
Path serverRoot = temporaryFolder.newFolder("husk-unconfigured").toPath();
Files.createDirectories(serverRoot.resolve("world/dimensions/iris/husk/data/minecraft"));
Files.writeString(serverRoot.resolve("world/dimensions/iris/husk/data/minecraft/raids.dat"), "x");
writeServerProperties(serverRoot);
writeBukkitWorlds(serverRoot);
List<String> warnings = new ArrayList<>();
IrisBootstrap.quarantineWorthlessHusks(startupPaths(serverRoot), warnings::add);
assertFalse(Files.exists(serverRoot.resolve("world/dimensions/iris/husk")));
assertEquals(1, warnings.size());
}
@Test
public void aWorldWithRealDataIsNeverQuarantinedAndStillStopsStartup() throws Exception {
Path serverRoot = temporaryFolder.newFolder("husk-with-data").toPath();
Files.createDirectories(serverRoot.resolve("world/dimensions/iris/kept/region"));
Files.writeString(serverRoot.resolve("world/dimensions/iris/kept/region/r.0.0.mca"), "terrain");
writeServerProperties(serverRoot);
writeBukkitWorlds(serverRoot, "world_iris_kept");
List<String> warnings = new ArrayList<>();
IrisBootstrap.quarantineWorthlessHusks(startupPaths(serverRoot), warnings::add);
assertTrue(Files.isDirectory(serverRoot.resolve("world/dimensions/iris/kept")));
assertTrue(warnings.isEmpty());
assertThrows(
IllegalStateException.class,
() -> IrisBootstrap.requireUsableWorldStorage(startupPaths(serverRoot)));
}
/**
* The engine rebuilds iris/engine-data under a world folder the server itself recreated during save-all,
* so an iris/ directory with no pack snapshot is the ambiguous state, not a worthless one.
*/
@Test
public void anIrisMarkerWithNoPackSnapshotStillFailsClosed() throws Exception {
Path serverRoot = temporaryFolder.newFolder("husk-marker").toPath();
Files.createDirectories(serverRoot.resolve("world/dimensions/iris/marked/iris/engine-data"));
writeServerProperties(serverRoot);
writeBukkitWorlds(serverRoot, "world_iris_marked");
List<String> warnings = new ArrayList<>();
IrisBootstrap.quarantineWorthlessHusks(startupPaths(serverRoot), warnings::add);
assertTrue(Files.isDirectory(serverRoot.resolve("world/dimensions/iris/marked/iris/engine-data")));
assertTrue(warnings.isEmpty());
assertThrows(
IllegalStateException.class,
() -> IrisBootstrap.requireUsableWorldStorage(startupPaths(serverRoot)));
}
@Test
public void startupContinuesForAWorldWhoseFolderIsGoneAndReportsItOnce() throws Exception {
Path serverRoot = temporaryFolder.newFolder("orphan-server").toPath();
Files.createDirectories(serverRoot.resolve("world/dimensions/iris"));
writeServerProperties(serverRoot);
writeBukkitWorlds(serverRoot, "world_iris_orphan1");
IrisBootstrap.requireUsableWorldStorage(startupPaths(serverRoot));
assertTrue(MissingWorldStorageLog.hasWarned("world_iris_orphan1"));
}
@Test
public void startupContinuesForHealthyWorlds() throws Exception {
Path serverRoot = temporaryFolder.newFolder("healthy-server").toPath();
Files.createDirectories(serverRoot.resolve("world/dimensions/iris/moon/iris/pack"));
writeServerProperties(serverRoot);
writeBukkitWorlds(serverRoot, "world_iris_moon");
IrisBootstrap.requireUsableWorldStorage(startupPaths(serverRoot));
assertFalse(MissingWorldStorageLog.hasWarned("world_iris_moon"));
}
private static BukkitStartupPaths startupPaths(Path serverRoot) throws Exception {
return BukkitStartupPaths.resolve(serverRoot, new String[0]);
}
private static void writeServerProperties(Path serverRoot) throws Exception {
Files.writeString(serverRoot.resolve("server.properties"), "level-name=world\n", StandardCharsets.UTF_8);
}
private static void writeBukkitWorlds(Path serverRoot, String... configuredWorldNames) throws Exception {
File configuration = serverRoot.resolve("bukkit.yml").toFile();
YamlConfiguration yaml = new YamlConfiguration();
for (String configuredWorldName : configuredWorldNames) {
yaml.set("worlds." + configuredWorldName + ".generator", "Iris:overworld");
}
yaml.save(configuration);
}
}
@@ -0,0 +1,89 @@
package art.arcane.iris;
import art.arcane.iris.spi.LogLevel;
import org.junit.Test;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.logging.Level;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
/**
* Core states a severity on every message it logs and the modded adapters honour it. On Bukkit the same
* message used to become a coloured console line, which the server logs at INFO, so no core warning ever
* appeared in a WARN-level scan of logs/latest.log - including the orphaned-world-storage reports the
* bootstrap replays there specifically for operators to find.
*/
public class IrisDiagnosticLogLevelTest {
@Test
public void coreDiagnosticsKeepTheirSeverity() {
assertEquals(Level.WARNING, Iris.diagnosticLevel(LogLevel.WARN));
assertEquals(Level.SEVERE, Iris.diagnosticLevel(LogLevel.ERROR));
}
@Test
public void informationalAndDebugMessagesStayOnTheConsolePath() {
assertNull(Iris.diagnosticLevel(LogLevel.INFO));
assertNull(Iris.diagnosticLevel(LogLevel.DEBUG));
}
/**
* Console sender output reaches the terminal but not the instance's logs/latest.log, which is the only
* log most operators read after the fact. A handful of lifecycle lines go to the plugin logger instead.
*/
@Test
public void lifecycleNoticesReachThePluginLoggerAtInfo() {
assertEquals(Level.INFO, Iris.diagnosticLevel(LogLevel.NOTICE));
}
/**
* A warning raised by the adapter is the same kind of thing as a warning raised by core. Routing one
* through the plugin logger and the other through the console sender makes the level depend on which
* side of the SPI the call happened to be written on.
*/
@Test
public void adapterSideWarningsCarryTheSameSeverityAsCoreWarnings() throws Exception {
String source = Files.readString(Path.of("src/main/java/art/arcane/iris/Iris.java"));
String warn = method(source, "public static void warn(String format, Object... objs)");
assertTrue(warn, warn.contains("diagnostic(Level.WARNING"));
assertFalse(warn, warn.contains("msg("));
String error = method(source, "public static void error(String format, Object... objs)");
assertTrue(error, error.contains("diagnostic(Level.SEVERE"));
assertFalse(error, error.contains("msg("));
}
@Test
public void theCoreLogBridgeRoutesDiagnosticsToThePluginLogger() throws Exception {
String source = Files.readString(Path.of("src/main/java/art/arcane/iris/Iris.java"));
String bridge = method(source, "private static void bridgeLog(LogLevel level, String message)");
assertTrue(bridge, bridge.contains("diagnosticLevel(target)"));
assertTrue(bridge, bridge.contains("diagnostic(diagnostic, message)"));
assertTrue(source.contains("plugin.getLogger().log(level, line)"));
}
private static String method(String source, String signature) {
int start = source.indexOf(signature);
assertTrue("method not found: " + signature, start >= 0);
int open = source.indexOf('{', start);
int depth = 0;
for (int i = open; i < source.length(); i++) {
char c = source.charAt(i);
if (c == '{') {
depth++;
} else if (c == '}') {
depth--;
if (depth == 0) {
return source.substring(start, i + 1);
}
}
}
throw new AssertionError("unterminated method: " + signature);
}
}
@@ -94,15 +94,97 @@ public class IrisWorldGeneratorResolverTest {
}
@Test
public void paperStartupAliasResolvesToCanonicalRuntimeKey() {
public void paperStartupAliasResolvesToCanonicalRuntimeKey() throws Exception {
File levelRoot = ownedLevelRoot("startup-alias", "moon");
assertEquals(
new NamespacedKey("iris", "moon"),
IrisWorldGeneratorResolver.configuredWorldKey("world_iris_moon", "world")
IrisWorldGeneratorResolver.configuredWorldKey("world_iris_moon", "world", levelRoot)
);
assertEquals(
new NamespacedKey("iris", "moon"),
IrisWorldGeneratorResolver.configuredWorldKey("moon", "world")
IrisWorldGeneratorResolver.configuredWorldKey("moon", "world", levelRoot)
);
assertEquals(
NamespacedKey.minecraft("overworld"),
IrisWorldGeneratorResolver.configuredWorldKey("world", "world", levelRoot)
);
}
@Test
public void multiverseRuntimeKeyedNameResolvesToTheStoredIrisWorld() throws Exception {
File levelRoot = ownedLevelRoot("runtime-keyed", "moon");
assertEquals(
"mv load names an Iris world iris_<key> because its keyed creator refuses the namespace",
new NamespacedKey("iris", "moon"),
IrisWorldGeneratorResolver.configuredWorldKey("iris_moon", "world", levelRoot)
);
}
@Test
public void runtimeKeyedNameWithoutStorageKeepsItsLiteralKey() throws Exception {
File levelRoot = ownedLevelRoot("runtime-keyed-missing", "elsewhere");
assertEquals(
new NamespacedKey("iris", "iris_moon"),
IrisWorldGeneratorResolver.configuredWorldKey("iris_moon", "world", levelRoot)
);
}
@Test
public void literalWorldStorageWinsOverTheRuntimeKeyedReading() throws Exception {
File levelRoot = ownedLevelRoot("runtime-keyed-collision", "moon");
assertTrue(new File(levelRoot, "dimensions/iris/iris_moon").mkdirs());
assertEquals(
"a world genuinely created as iris_moon keeps its own identity",
new NamespacedKey("iris", "iris_moon"),
IrisWorldGeneratorResolver.configuredWorldKey("iris_moon", "world", levelRoot)
);
}
@Test
public void levelNamedIrisStillResolvesItsOwnStartupNames() throws Exception {
File levelRoot = temporaryFolder.newFolder("level-named-iris", "iris");
assertTrue(new File(levelRoot, "dimensions/iris/moon").mkdirs());
assertEquals(
new NamespacedKey("iris", "moon"),
IrisWorldGeneratorResolver.configuredWorldKey("iris_iris_moon", "iris", levelRoot)
);
}
/**
* A refusal has to name the world the admin typed. The generation guard keeps the storage-existence
* check that stops a genuine iris_moon world being mis-mapped; the message must not, or an orphan is
* described as iris:iris_orphan1 and the remedy names a world that never existed.
*/
@Test
public void refusalMessagesNameTheRuntimeKeyedWorldWithoutItsStorage() {
assertEquals(
new NamespacedKey("iris", "orphan1"),
IrisWorldGeneratorResolver.messageWorldKey("iris_orphan1", "world")
);
assertEquals(
new NamespacedKey("iris", "moon"),
IrisWorldGeneratorResolver.messageWorldKey("world_iris_moon", "world")
);
assertEquals(
"a world genuinely created as iris_moon keeps its own identity",
new NamespacedKey("iris", "iris_moon"),
IrisWorldGeneratorResolver.messageWorldKey("world_iris_iris_moon", "world")
);
assertEquals(
NamespacedKey.minecraft("overworld"),
IrisWorldGeneratorResolver.messageWorldKey("world", "world")
);
}
private File ownedLevelRoot(String scope, String worldKey) throws Exception {
File levelRoot = temporaryFolder.newFolder(scope, "world");
assertTrue(new File(levelRoot, "dimensions/iris/" + worldKey).mkdirs());
return levelRoot;
}
@Test
@@ -76,7 +76,7 @@ public class IrisApiWiringContractTest {
assertBefore(add, "registered = true;", "phases.ready(world)");
String remove = method(source,
"private void remove(World world, CompletionStage<Boolean> unloadBoundary)");
"private boolean remove(World world, CompletionStage<Boolean> unloadBoundary)");
assertBefore(remove, "registered = worlds.remove(world)", "phases.closing(world)");
assertBefore(remove, "phases.closing(world)",
"deferCloseUntilWorldUnload(world, registered, closing, unloadBoundary)");
@@ -49,7 +49,7 @@ public class IrisEngineLifecycleContractTest {
public void registrationRetryWaitsAsynchronouslyForClose() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.engineSvcSource")));
String add = method(source, "private void add(World world)");
String remove = method(source, "private void remove(World world, CompletionStage<Boolean> unloadBoundary)");
String remove = method(source, "private boolean remove(World world, CompletionStage<Boolean> unloadBoundary)");
String completeClose = method(source, "private void completeClose(");
String retry = method(source, "private void retryRegistrationAfterClose(");
String invokeClose = method(source, "private CompletableFuture<Void> invokeGeneratorClose()");
@@ -73,7 +73,7 @@ public class IrisEngineLifecycleContractTest {
public void worldUnloadDefersGeneratorCloseUntilTheRawBoundaryCompletesTrue() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.engineSvcSource")));
String handler = method(source, "public void onWorldUnload(WorldUnloadEvent event)");
String remove = method(source, "private void remove(World world, CompletionStage<Boolean> unloadBoundary)");
String remove = method(source, "private boolean remove(World world, CompletionStage<Boolean> unloadBoundary)");
String defer = method(source, "private void deferCloseUntilWorldUnload(");
assertBefore(handler, "WorldUnloadBoundaryRegistry.claim(", "remove(world, unloadBoundary)");
@@ -0,0 +1,59 @@
package art.arcane.iris.core.service;
import art.arcane.iris.engine.platform.BukkitChunkGenerator;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.bukkit.event.world.WorldUnloadEvent;
import org.junit.Test;
import java.util.concurrent.CompletableFuture;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* Multiverse unloads a world through {@code Bukkit.unloadWorld}, which carries no Iris unload boundary.
* {@code IrisEngineSVC} closes the engine of every world it registered, but registration is skipped while
* a previous generator for the same identity is still closing, and an unload landing in that window used
* to leave a live engine bound to a dead world.
*/
public class IrisEngineSVCUntrackedUnloadTest {
@Test
public void externalUnloadClosesAnIrisGeneratorTheServiceNeverRegistered() {
BukkitChunkGenerator generator = mock(BukkitChunkGenerator.class);
when(generator.closeAsync()).thenReturn(CompletableFuture.completedFuture(null));
new IrisEngineSVC().onWorldUnload(new WorldUnloadEvent(irisWorld("mvtest", generator)));
verify(generator).closeAsync();
}
@Test
public void externalUnloadOfANonIrisWorldClosesNothing() {
World world = mock(World.class);
when(world.getName()).thenReturn("plots");
when(world.getKey()).thenReturn(new NamespacedKey("minecraft", "plots"));
new IrisEngineSVC().onWorldUnload(new WorldUnloadEvent(world));
}
@Test
public void externalUnloadDoesNotReenterAGeneratorThatIsAlreadyClosing() {
BukkitChunkGenerator generator = mock(BukkitChunkGenerator.class);
when(generator.isClosing()).thenReturn(true);
new IrisEngineSVC().onWorldUnload(new WorldUnloadEvent(irisWorld("closing", generator)));
verify(generator, never()).closeAsync();
}
private static World irisWorld(String key, BukkitChunkGenerator generator) {
World world = mock(World.class);
when(world.getName()).thenReturn("world_iris_" + key);
when(world.getKey()).thenReturn(new NamespacedKey("iris", key));
when(world.getGenerator()).thenReturn(generator);
return world;
}
}