This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-11 10:35:59 -04:00
1 parent d480abad50
commit 2145e90b1c
46 files changed
+1766 -167

No files matched your search

@@ -0,0 +1,71 @@
import { readFile, writeFile } from 'node:fs/promises'
import path from 'node:path'
import { setup, openStudio, closeStudio, sampleTerrain, readJson, writeJson,
logPosition, logSince } from './studio-support.mjs'
export default {
name: 'iris-studio-hotload-lifecycle',
description: 'Generate cold chunks during Studio edits and close after saving an edit.',
async run(context) {
const fixture = await setup(context)
const biomeFile = path.join(fixture.pack, 'biomes', 'flat.json')
const original = await readFile(biomeFile, 'utf8')
const logOffset = await logPosition(fixture)
let active
let x = 16384
try {
active = await openStudio(context, fixture)
await sampleTerrain(context, fixture, 0, fixture.state.material)
for (let cycle = 0; cycle < 3; cycle++) {
await context.step(`Load cold chunks during hotload ${cycle + 1}`, async () => {
const manifestFile = fixture.manifestPath(active.key)
const before = await readJson(manifestFile)
const biome = await readJson(biomeFile)
const material = cycle % 2 === 0 ? 'gold_block' : 'diamond_block'
biome.name = `Lifecycle terrain ${cycle + 1}`
biome.layers[0].palette[0].block = `minecraft:${material}`
const startedAt = Date.now()
await writeJson(biomeFile, biome)
x += 512
await context.command(`/minecraft:tp @s ${x + 0.5} 101 0.5`, /Teleported/i, 15000)
const after = await fixture.wait(async () => {
const manifest = await readJson(manifestFile)
return manifest.activeActivationId !== before.activeActivationId ? manifest : false
}, 'Hotload did not complete while the player requested fresh chunks', 90000)
const activationDurationMs = Date.now() - startedAt
await fixture.wait(() => Math.abs(context.bot.entity.position.x - x - 0.5) < 1,
'Cold chunk teleport did not arrive after hotload', 30000)
context.report.iris.activations.push({ before: before.activeActivationId,
after: after.activeActivationId, material, coldChunkRequests: 1, activationDurationMs })
x += 2048
await sampleTerrain(context, fixture, x, material)
})
}
await context.step('Close Studio while a saved edit is activating', async () => {
const biome = await readJson(biomeFile)
biome.name = 'Lifecycle close terrain'
await writeJson(biomeFile, biome)
const pending = await fixture.wait(async () => {
const manifest = await readJson(fixture.manifestPath(active.key))
return manifest.pendingActivationId !== null ? manifest : false
}, 'The saved close edit did not enter activation', 90000)
context.report.iris.closePendingActivationId = pending.pendingActivationId
await closeStudio(context, fixture, active)
active = undefined
await context.command('/minecraft:seed', /Seed:/i, 10000)
})
await context.step('Reopen the edited pack and retain usable generation', async () => {
active = await openStudio(context, fixture)
await sampleTerrain(context, fixture, 0, 'gold_block')
await closeStudio(context, fixture, active)
active = undefined
})
const log = await logSince(fixture, logOffset)
context.expect(!/Studio generation update failed|Unable to (capture terrain|save the terrain boundary)|ClosedChannelException|Chunk system error|server has stopped responding|could not acquire its engine runtime|Failed to list files/i.test(log),
'Studio lifecycle emitted a generation, snapshot, storage, or watchdog failure')
} finally {
if (active) await closeStudio(context, fixture, active)
await writeFile(biomeFile, original)
}
}
}
@@ -20,33 +20,20 @@ import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
public class PaperPluginMetadataTest {
private static final List<String> OPTIONAL_PLUGIN_IDS = List.of(
private static final List<String> JOINED_PLUGIN_IDS = List.of(
"PlaceholderAPI",
"CraftEngine",
"Nexo",
"Oraxen",
"ItemsAdder",
"SCore",
"ExecutableItems",
"MythicLib",
"MMOItems",
"eco",
"EcoItems",
"MythicMobs",
"MythicCrucible",
"KGenerators",
"Multiverse-Core",
"WorldEdit"
);
private static final List<String> JOINED_PLUGIN_IDS = OPTIONAL_PLUGIN_IDS.stream()
.filter(pluginId -> !"ExecutableItems".equals(pluginId) && !"Multiverse-Core".equals(pluginId))
.toList();
private static final List<String> UNORDERED_PLUGIN_IDS = List.of(
"MythicLib", "MMOItems", "MythicMobs", "MythicCrucible");
private static final List<String> BUKKIT_SOFT_DEPEND_IDS = OPTIONAL_PLUGIN_IDS.stream()
.filter(pluginId -> !"Multiverse-Core".equals(pluginId))
.filter(pluginId -> !UNORDERED_PLUGIN_IDS.contains(pluginId))
.toList();
@Test
public void paperMetadataDeclaresBootstrapAndFoliaSupport() throws Exception {
@@ -75,10 +62,13 @@ public class PaperPluginMetadataTest {
+ " description: Allows survival players to fell Iris-managed trees with an axe.\n"
+ " default: op"));
for (String pluginId : JOINED_PLUGIN_IDS) {
String loadOrder = UNORDERED_PLUGIN_IDS.contains(pluginId) ? "OMIT" : "BEFORE";
assertTrue(metadata.contains(optionalDependencyBlock(pluginId, loadOrder, true)));
assertTrue(metadata.contains(optionalDependencyBlock(pluginId, "OMIT", true)));
}
assertTrue(metadata.contains(optionalDependencyBlock("ExecutableItems", "BEFORE", false)));
assertFalse(metadata.contains(" ExecutableItems:"));
assertFalse(metadata.contains(" EcoItems:"));
assertFalse(metadata.contains(" MythicLib:"));
assertEquals(1, metadata.lines().filter(line -> line.equals(" load: AFTER")).count());
assertEquals(0, metadata.lines().filter(line -> line.equals(" load: BEFORE")).count());
assertTrue(metadata.contains(optionalDependencyBlock("Multiverse-Core", "AFTER", true)));
}
@@ -95,7 +85,7 @@ public class PaperPluginMetadataTest {
Map<String, Map<String, Object>> commands = metadata.getCommands();
assertTrue(commands.containsKey("iris"));
assertEquals(List.of("ir", "irs"), commands.get("iris").get("aliases"));
assertEquals(BUKKIT_SOFT_DEPEND_IDS, metadata.getSoftDepend());
assertTrue(metadata.getSoftDepend().isEmpty());
assertEquals(List.of("Multiverse-Core"), metadata.getLoadBeforePlugins());
Permission all = metadata.getPermissions().stream()
.filter(permission -> "iris.all".equals(permission.getName()))
@@ -0,0 +1,103 @@
package art.arcane.iris.integration;
import art.arcane.iris.Iris;
import art.arcane.volmlib.util.bukkit.papi.PlaceholderRegistration;
import org.bukkit.Bukkit;
import org.bukkit.event.HandlerList;
import org.bukkit.event.server.PluginDisableEvent;
import org.bukkit.event.server.PluginEnableEvent;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.PluginManager;
import org.junit.Test;
import org.mockito.MockedConstruction;
import org.mockito.MockedStatic;
import java.lang.reflect.Field;
import java.util.logging.Logger;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doCallRealMethod;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockConstruction;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class IrisPapiLifecycleTest {
@Test
public void lateEnableRegistersOnceAndReattachesAfterDisable() throws Exception {
Iris iris = plugin(true);
Plugin placeholders = namedPlugin("PlaceholderAPI");
PluginManager plugins = mock(PluginManager.class);
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
MockedStatic<HandlerList> handlers = mockStatic(HandlerList.class);
MockedStatic<PlaceholderRegistration> availability = mockStatic(PlaceholderRegistration.class);
MockedStatic<IrisPapiInstaller> installer = mockStatic(IrisPapiInstaller.class);
MockedConstruction<PlaceholderRegistration> registrations = mockConstruction(PlaceholderRegistration.class)) {
bukkit.when(Bukkit::getPluginManager).thenReturn(plugins);
availability.when(PlaceholderRegistration::isPlaceholderApiEnabled).thenReturn(true);
installer.when(() -> IrisPapiInstaller.install(any(), any(), any())).thenReturn(true);
iris.onPlaceholderApiEnable(new PluginEnableEvent(placeholders));
iris.onPlaceholderApiEnable(new PluginEnableEvent(placeholders));
assertEquals(1, registrations.constructed().size());
verify(plugins).registerEvents(any(IrisPapiListener.class), eq(iris));
iris.onPlaceholderApiDisable(new PluginDisableEvent(namedPlugin("Unrelated")));
verify(registrations.constructed().getFirst(), never()).unregister();
iris.onPlaceholderApiDisable(new PluginDisableEvent(placeholders));
verify(registrations.constructed().getFirst()).unregister();
handlers.verify(() -> HandlerList.unregisterAll(any(IrisPapiListener.class)));
iris.onPlaceholderApiEnable(new PluginEnableEvent(placeholders));
assertEquals(2, registrations.constructed().size());
verify(plugins, times(2)).registerEvents(any(IrisPapiListener.class), eq(iris));
}
}
@Test
public void enableEventsCannotRegisterBeforeIrisIsReadyOrDuringShutdown() throws Exception {
Iris iris = plugin(false);
Plugin placeholders = namedPlugin("PlaceholderAPI");
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
MockedStatic<IrisPapiInstaller> installer = mockStatic(IrisPapiInstaller.class)) {
iris.onPlaceholderApiEnable(new PluginEnableEvent(placeholders));
installer.verifyNoInteractions();
}
}
@Test
public void unrelatedEnableEventsDoNotRegisterPlaceholders() throws Exception {
Iris iris = plugin(true);
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
MockedStatic<IrisPapiInstaller> installer = mockStatic(IrisPapiInstaller.class)) {
iris.onPlaceholderApiEnable(new PluginEnableEvent(namedPlugin("Unrelated")));
installer.verifyNoInteractions();
}
}
private static Iris plugin(boolean active) throws ReflectiveOperationException {
Iris iris = mock(Iris.class);
when(iris.getLogger()).thenReturn(Logger.getAnonymousLogger());
doCallRealMethod().when(iris).onPlaceholderApiEnable(any());
doCallRealMethod().when(iris).onPlaceholderApiDisable(any());
Field lifecycle = Iris.class.getDeclaredField("papiLifecycleActive");
lifecycle.setAccessible(true);
lifecycle.setBoolean(iris, active);
return iris;
}
private static Plugin namedPlugin(String name) {
Plugin plugin = mock(Plugin.class);
when(plugin.getName()).thenReturn(name);
return plugin;
}
}