Merge master into 3D terrain

- Resolve IrisTerrainNormalActuator by keeping master's primitive riverWaterSurfaceStream lookup and primarySurfaceLayerOrNull accessors while retaining the terrain column, span, layer surface and ceiling locals that write air through terrain3D gaps
- Resolve HydrologyFootprintCompiler by keeping the MaterializedSurface planned-surface record with terrain ownership beside master's package-private SurfaceRasterIndex, which retains its ownsTerrain override
- Resolve HydrologyCaveContainmentPlanner by dropping the nested RecordingCaveVoxelView, SurfaceObservations and ViewObservations types that master promoted to their own files
- Port the surface-observation ownership check into CaveSurfaceObservations so cached cave validations replay both height and ownership observations through Observation.matches
- Resolve PackExportClosureTest by keeping master's retirement of the source-text packager guard while retaining the snippet export tests, and restore the Files and Path imports those tests use
- Resolve IrisBiomeLayerSlopeSourceTest by folding the lower-ledge slope palette case into master's lockLayers parameterization
- Recapture the studio schema digest for the terrain3D biome property, its IrisTerrain3D definition, the terrain-3d snippet enumeration and the reworded upperDimensionCarving description, verified by reconstructing the previous digest from the stripped schema
This commit is contained in:
Brian Neumann-Fopiano committed 2026-09-08 13:58:00 -04:00
commit 98e51cba8d
520 files changed
+45623 -40734

No files matched your search

@@ -1,182 +0,0 @@
package art.arcane.iris.core;
import org.junit.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.stream.Stream;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
/**
* Every core diagnostic keeps its severity through to the server log, so the level a call site picks is the
* level an operator scanning for WARN and SEVERE sees. This pins the two ends of that decision: conditions an
* operator must act on stay at WARN or ERROR, and conditions that repeat per block, per sample or per chunk -
* or that report designed backpressure, a probe on an optional dependency, or a success - do not.
* <p>
* The pin is on the source rather than on a live engine because most of these call sites need a running world
* to reach, and what is being fixed is the choice of level, not the condition that triggers it.
*/
public class DiagnosticSeverityPolicyTest {
@Test
public void perBlockAndPerSampleConditionsDoNotReachTheServerLogAtWarnLevel() {
assertLoggedWith("util/project/hunk/Hunk.java", "OUT OF BOUNDS ", "debug");
assertLoggedWith("engine/mantle/MantleWriter.java", "No set? ", "debug");
assertLoggedWith("engine/mantle/MantleWriter.java", "Mantle Writer Accessed chunk out of bounds", "debug");
assertLoggedWith("engine/object/IrisDecorator.java", "Empty Block Data for ", "warnOnce");
assertLoggedWith("engine/object/IrisCompat.java", "Can't find block data for ", "warnOnce");
assertLoggedWith("engine/data/cache/AtomicCache.java", "Atomic cache supplier failed: %s: %s", "warnOnce");
assertLoggedWith("engine/IrisComplex.java", "Failed to sample interpolated biome bounds", "warnOnce");
assertLoggedWith("engine/framework/placer/WorldObjectPlacer.java", "Tried to place custom block at", "warnOnce");
}
/**
* Backpressure pausing generation is the design working. It is reported every five seconds for the whole
* duration of a large pregen, so at WARN it would be the only thing a log scan finds.
*/
@Test
public void designedBackpressureIsReportedWithoutRaisingAWarning() {
assertLoggedWith("core/pregenerator/PregenMantleBackpressure.java", "Pregen mantle backpressure: ", "info");
assertLoggedWith("core/pregenerator/PregenMantleBackpressure.java", "Pregen heap pressure: pausing generation", "info");
assertLoggedWith("core/pregenerator/MantleHeapPressure.java", "Iris heap remained at", "info");
assertLoggedWith("core/pregenerator/methods/AsyncPregenMethod.java", "is still pending after", "debug");
}
@Test
public void generationTracingStaysAtDebugLevel() {
assertLoggedWith("engine/mantle/components/MantleObjectComponent.java", "Regen object layer start:", "debug");
assertLoggedWith("engine/mantle/components/MantleObjectComponent.java", "Goldendebug object attempt:", "debug");
assertLoggedWith("engine/mantle/components/GoldenDebugObjectPlacer.java", "Goldendebug query:", "debug");
assertLoggedWith("engine/mantle/components/IrisStructureComponent.java", "[StructTrace] ORIGIN", "debug");
}
@Test
public void coreProductionDoesNotWriteDirectlyToProcessStreams() throws IOException {
try (Stream<Path> files = Files.walk(Path.of("src/main/java"))) {
List<Path> bypasses = files
.filter(path -> path.toString().endsWith(".java"))
.filter(path -> {
try {
String source = Files.readString(path);
return source.contains("System.out")
|| source.contains("System.err")
|| source.contains(".printStackTrace();");
} catch (IOException unreadable) {
throw new IllegalStateException(unreadable);
}
})
.toList();
assertTrue(bypasses.toString(), bypasses.isEmpty());
}
}
@Test
public void probesOnOptionalDependenciesAreNotWarnings() throws IOException {
assertLoggedWith("core/link/MultiverseCoreLink.java", "is not reachable; Iris world settings", "info");
assertLoggedWith("core/link/MultiverseCoreLink.java", "Multiverse will record the live name", "info");
assertLoggedWith("core/gui/GuiHost.java", "Unable to install the Iris desktop quit guard", "info");
String slimJar = read("util/common/misc/SlimJar.java");
assertTrue(slimJar.contains("debug(plugin, \"Failed to inject the library loader"));
assertTrue(slimJar.contains("if (DEBUG)"));
assertTrue(slimJar.contains("plugin.getLogger().info(\"[DEBUG] \" + message)"));
}
/**
* A refused destructive command is the guard working, and the admin who typed it is told directly. The
* console record of it is not a warning about the server.
*/
@Test
public void aRefusedMultiverseCommandIsRecordedWithoutRaisingAWarning() throws IOException {
String guard = read("core/link/MultiverseGuardListener.java");
assertTrue("the console record of a refusal is kept", guard.contains("IrisLogging.info(\"%s %s\", reason, remedy)"));
assertFalse("the refusal is not a warning about the server", guard.contains("IrisLogging.warn(\"%s %s\""));
}
@Test
public void decorationAndBlankLinesAreNotEmittedAtAnySeverity() throws IOException {
String safeguard = read("core/safeguard/IrisSafeguard.java");
assertFalse("a blank record renders as an empty [WARN] line", safeguard.contains("IrisLogging.warn(\"\")"));
assertFalse("a blank record renders as an empty [SEVERE] line", safeguard.contains("IrisLogging.error(\"\")"));
assertFalse("a blank record renders as an empty [INFO] line", safeguard.contains("IrisLogging.info(\"\")"));
assertFalse("a separator rule is not a diagnostic", safeguard.contains("--==<"));
String configurator = read("core/ServerConfigurator.java");
assertFalse("a separator rule is not a diagnostic", configurator.contains("IrisLogging.error(\"===="));
assertFalse("a separator rule is not a diagnostic", configurator.contains("IrisLogging.error(\"----"));
}
/**
* A written debug image is a success, and the method that wrote it had no callers at all.
*/
@Test
public void theUnusedDebugImageWriterIsGone() throws IOException {
assertFalse(read("engine/object/IrisImage.java").contains("writeDebug"));
}
/**
* The other end of the policy. These are the conditions the promotion exists for.
*/
@Test
public void operatorActionableConditionsKeepTheirSeverity() {
assertLoggedWith("core/lifecycle/VanishedWorldStorage.java", "Iris world storage is gone at", "error");
assertLoggedWith("core/IrisWorlds.java", "has unusable world storage and is excluded", "error");
assertLoggedWith("engine/EngineDataStore.java", "Failed to setup Engine Data", "error");
assertLoggedWith("engine/IrisEngineMantle.java", "Failed to read chunk section, skipping it.", "error");
assertLoggedWith("engine/IrisEngineMantle.java", "Failed to read chunk, creating a new chunk instead.", "error");
assertLoggedWith("util/common/reflect/WrappedField.java", "Failed to created WrappedField", "error");
assertLoggedWith("core/datapack/DatapackIngestService.java", "Repairing modified or corrupt Iris-managed datapack", "warn");
assertLoggedWith("core/pregenerator/cache/PregenCacheImpl.java", "Pregen cache storage is gone at", "warn");
assertLoggedWith("core/pregenerator/PregenMantleBackpressure.java", "Pregen mantle backpressure exceeded ", "warn");
assertLoggedWith("core/pregenerator/PregenMantleBackpressure.java", "Pregen heap pressure wait exceeded ", "warn");
assertLoggedWith("core/pregenerator/IrisPregenerator.java", "failed chunk(s); failures are not cached", "warn");
assertLoggedWith("core/service/MultiverseSVC.java", "Multiverse world events are not bindable", "warn");
}
/**
* A handful of lifecycle lines an operator reads logs/latest.log to find. NOTICE is the level adapters
* route to the server's own logger rather than to the console sender.
*/
@Test
public void lifecycleLinesAreRaisedAtNoticeLevel() {
assertLoggedWith("engine/IrisEngine.java", "Engine init: ", "notice");
assertLoggedWith("core/link/MultiverseCoreLink.java", "Adopted %d live Iris world", "notice");
assertLoggedWith("core/pregenerator/IrisPregenerator.java", "Pregen finished: ", "notice");
}
private static void assertLoggedWith(String relativePath, String fragment, String expectedMethod) {
String source;
try {
source = read(relativePath);
} catch (IOException unreadable) {
throw new AssertionError("Cannot read " + relativePath, unreadable);
}
int occurrences = 0;
for (int at = source.indexOf(fragment); at >= 0; at = source.indexOf(fragment, at + 1)) {
occurrences++;
assertEquals(relativePath + " @ \"" + fragment + "\" occurrence " + occurrences,
expectedMethod, loggingMethodBefore(source, at, relativePath, fragment));
}
assertTrue(relativePath + " no longer contains \"" + fragment + "\"", occurrences > 0);
}
private static String loggingMethodBefore(String source, int at, String relativePath, String fragment) {
int call = source.lastIndexOf("IrisLogging.", at);
assertTrue(relativePath + " @ \"" + fragment + "\" is not logged through IrisLogging", call >= 0);
int start = call + "IrisLogging.".length();
int end = start;
while (end < source.length() && Character.isJavaIdentifierPart(source.charAt(end))) {
end++;
}
return source.substring(start, end);
}
private static String read(String relativePath) throws IOException {
return Files.readString(Path.of("src/main/java/art/arcane/iris").resolve(relativePath)).replace("\r\n", "\n");
}
}
@@ -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();
@@ -1,6 +1,7 @@
package art.arcane.iris.core;
import art.arcane.iris.testsupport.BukkitTestServer;
import org.junit.Rule;
import org.junit.Test;
@@ -9,6 +10,9 @@ import static org.junit.Assume.assumeTrue;
import static org.mockito.Mockito.mockingDetails;
public class IrisRuntimeSchedulerModeRoutingTest {
@Rule
public final BukkitTestServer.ServerBranding branding = BukkitTestServer.branding();
@Test
public void autoResolvesToPaperLikeOnPurpurBranding() {
installServer("Purpur", "git-Purpur-2562 (MC: 26.2)");
@@ -55,6 +59,6 @@ public class IrisRuntimeSchedulerModeRoutingTest {
private void installServer(String name, String version) {
assumeTrue(mockingDetails(BukkitTestServer.install()).isMock());
BukkitTestServer.brand(name, version);
branding.set(name, version);
}
}
@@ -0,0 +1,140 @@
package art.arcane.iris.core;
import art.arcane.iris.core.safeguard.IrisSafeguard;
import art.arcane.iris.core.safeguard.Mode;
import art.arcane.iris.core.safeguard.task.CheckResult;
import art.arcane.iris.core.safeguard.task.Diagnostic;
import art.arcane.iris.core.safeguard.task.Task;
import art.arcane.iris.core.safeguard.task.Tasks;
import org.junit.After;
import org.junit.Test;
import org.mockito.MockedStatic;
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;
import static org.mockito.Mockito.mockStatic;
/**
* Danger Mode used to be a label: only the check whose id happened to be {@code injection} locked the
* runtime, and it did so from inside a catch that string-matched that id. Every other critical failure -
* an NMS binding that could not be bound, {@code general.disableNMS}, missing dimension types - printed
* Danger and then let the boot carry on with logins open. The severity a check declares is now the whole
* decision: any Danger result locks the runtime, and every check states the lock reason an operator reads.
*/
public class IrisSafeguardLockPolicyTest {
@After
public void resetValidationAndSafeguard() {
IrisStartupValidation.disable();
try (MockedStatic<Tasks> tasks = mockStatic(Tasks.class)) {
tasks.when(Tasks::getTasks).thenReturn(List.of());
IrisSafeguard.execute();
}
}
@Test
public void aDangerResultFromAnyCheckLocksTheRuntime() {
Task dimensionTypes = Task.critical("dimensionTypes", "unused throw reason",
() -> CheckResult.danger("Iris dimension types were not registered. Restart the server so the registries reload.",
Diagnostic.Logger.ERROR.create("Dimension Types")));
execute(dimensionTypes);
assertEquals(Mode.UNSTABLE, IrisSafeguard.mode());
assertFalse(IrisStartupValidation.isReady());
assertEquals("Iris dimension types were not registered. Restart the server so the registries reload.",
IrisStartupValidation.denialReason().orElseThrow());
assertThrows(IllegalStateException.class, IrisStartupValidation::requireWorldCreationReady);
}
@Test
public void anAdvisoryCheckThatThrowsStaysWarningAndLeavesTheRuntimeReady() {
Task memory = Task.advisory("memory", () -> {
throw new IllegalStateException("no hardware probe");
});
execute(memory);
assertEquals(Mode.WARNING, IrisSafeguard.mode());
assertTrue(IrisStartupValidation.isReady());
assertEquals("warning", IrisSafeguard.asContext().get("memory"));
}
@Test
public void aCriticalCheckThatThrowsLocksWithTheReasonItDeclared() {
Task injection = Task.critical("injection",
"Iris runtime injection failed. Resolve the startup errors and restart the server.",
() -> {
throw new IllegalStateException("Java agent unavailable");
});
execute(injection);
assertEquals(Mode.UNSTABLE, IrisSafeguard.mode());
assertEquals("Iris runtime injection failed. Resolve the startup errors and restart the server.",
IrisStartupValidation.denialReason().orElseThrow());
}
@Test
public void theFirstDangerCheckOwnsTheLockReason() {
Task version = Task.critical("version", "unused throw reason",
() -> CheckResult.danger("first reason", Diagnostic.Logger.ERROR.create("Server Version")));
Task injection = Task.critical("injection", "unused throw reason",
() -> CheckResult.danger("second reason", Diagnostic.Logger.ERROR.create("Java Agent")));
execute(version, injection);
assertEquals("first reason", IrisStartupValidation.denialReason().orElseThrow());
}
@Test
public void everyCheckStableLeavesTheRuntimeReady() {
execute(Task.advisory("memory", CheckResult::stable), Task.critical("injection", "unused", CheckResult::stable));
assertEquals(Mode.STABLE, IrisSafeguard.mode());
assertTrue(IrisStartupValidation.isReady());
}
/**
* A Danger result has to name what the operator must fix, so a check cannot raise one without a reason.
*/
@Test
public void aDangerResultWithoutALockReasonIsRejected() {
assertThrows(IllegalArgumentException.class,
() -> CheckResult.danger(" ", Diagnostic.Logger.ERROR.create("Broken")));
}
/**
* The per-check modes and diagnostic lines were collected on every boot and read by nothing.
* {@code /iris debugdump} carries them now, so a support report says which check failed.
*/
@Test
public void theDebugReportNamesEveryCheckWithItsModeAndDiagnosticLines() {
Task memory = Task.advisory("memory", () -> CheckResult.warning(
Diagnostic.Logger.WARN.create("Low Memory"),
Diagnostic.Logger.WARN.create("- JVM maximum heap: 1024 MB")));
Task injection = Task.critical("injection", "unused", CheckResult::stable);
execute(memory, injection);
String report = IrisSafeguard.debugReport();
assertTrue(report, report.contains("Startup safeguard: warning"));
assertTrue(report, report.contains("memory: warning"));
assertTrue(report, report.contains("Low Memory"));
assertTrue(report, report.contains("- JVM maximum heap: 1024 MB"));
assertTrue(report, report.contains("injection: stable"));
}
private void execute(Task... checks) {
IrisStartupValidation.begin();
IrisStartupValidation.markDatapacksReady();
IrisStartupValidation.markPacksReady();
try (MockedStatic<Tasks> tasks = mockStatic(Tasks.class)) {
tasks.when(Tasks::getTasks).thenReturn(List.of(checks));
IrisSafeguard.execute();
}
}
}
@@ -15,6 +15,7 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mockStatic;
public class IrisSafeguardTest {
@@ -30,9 +31,11 @@ public class IrisSafeguardTest {
@Test
public void injectionExceptionRevokesReadinessAndReportsUnstableRuntime() {
IllegalStateException injectionFailure = new IllegalStateException("Java agent unavailable");
Task injection = Task.of("injection", () -> {
throw injectionFailure;
});
Task injection = Task.critical("injection",
"Iris runtime injection failed. Resolve the startup errors and restart the server.",
() -> {
throw injectionFailure;
});
IrisStartupValidation.begin();
IrisStartupValidation.markDatapacksReady();
IrisStartupValidation.markPacksReady();
@@ -53,7 +56,7 @@ public class IrisSafeguardTest {
assertEquals("Iris runtime injection failed. Resolve the startup errors and restart the server.",
IrisStartupValidation.denialReason().orElseThrow());
assertThrows(IllegalStateException.class, IrisStartupValidation::requireWorldCreationReady);
logging.verify(() -> IrisLogging.reportError(injectionFailure));
logging.verify(() -> IrisLogging.reportError(anyString(), org.mockito.ArgumentMatchers.same(injectionFailure)));
}
}
}
@@ -0,0 +1,79 @@
package art.arcane.iris.core;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.File;
import java.nio.file.Files;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.contains;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
/**
* Iris rewrites iris.json on every read so a file written by an older build gains the keys it is missing.
* That write used to fail into an empty catch, which is how a read-only or full data folder turned every
* settings change an operator made in game into something that silently vanished on the next boot.
*/
public class IrisSettingsRewriteFailureTest {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
private IrisPlatform capturingPlatform;
private IrisPlatform previousPlatform;
private IrisSettings previousSettings;
@Before
public void captureReports() {
previousSettings = IrisSettings.settings;
previousPlatform = IrisPlatforms.isBound() ? IrisPlatforms.get() : null;
IrisPlatforms.unbind();
capturingPlatform = mock(IrisPlatform.class);
IrisPlatforms.bind(capturingPlatform);
}
@After
public void restore() {
IrisPlatforms.unbind();
if (previousPlatform != null) {
IrisPlatforms.bind(previousPlatform);
}
IrisSettings.settings = previousSettings;
}
@Test
public void anUnwritableSettingsFileIsReported() throws Exception {
File settingsFile = temporaryFolder.newFile("iris.json");
Files.writeString(settingsFile.toPath(), "{}");
assertTrue("this test needs a file the JVM can mark read-only", settingsFile.setWritable(false));
doReturn(settingsFile).when(capturingPlatform).dataFile("iris.json");
IrisSettings.invalidate();
IrisSettings.get();
verify(capturingPlatform).reportError(contains(settingsFile.getAbsolutePath()), any(Throwable.class));
}
@Test
public void aWritableSettingsFileIsRewrittenQuietly() throws Exception {
File settingsFile = temporaryFolder.newFile("iris.json");
Files.writeString(settingsFile.toPath(), "{}");
doReturn(settingsFile).when(capturingPlatform).dataFile("iris.json");
IrisSettings.invalidate();
IrisSettings.get();
verify(capturingPlatform, never()).reportError(anyString(), any(Throwable.class));
assertTrue(Files.readString(settingsFile.toPath()).contains("general"));
}
}
@@ -95,28 +95,6 @@ public class IrisWorldsTest {
* asserted against the source: one unusable world folder used to throw out of {@code clean()}, through
* the constructor and into the cache, which returned null and NPE'd every caller.
*/
@Test
public void oneUnusableWorldIsExcludedInsteadOfFailingTheWholeRegistry() throws Exception {
String source = Files.readString(Path.of("src/main/java/art/arcane/iris/core/IrisWorlds.java")).replace("\r\n", "\n");
int accessor = source.indexOf("public static IrisWorlds get()");
int accessorEnd = source.indexOf("public synchronized void put(", accessor);
assertTrue("get() must not swallow a failure into a null return",
source.substring(accessor, accessorEnd).contains("cache.aquireOnceOrThrow("));
int clean = source.indexOf("public synchronized void clean()");
int cleanEnd = source.indexOf("public synchronized void save()", clean);
String cleanBody = source.substring(clean, cleanEnd);
assertTrue("clean() must isolate an unusable entry",
cleanBody.contains("catch (IllegalStateException e)"));
assertTrue("an unusable entry stays in the registry so /iris remove can still find it",
cleanBody.contains("warnUnusableStorage(entry.getKey(), e);"));
int loadDimension = source.indexOf("private IrisDimension loadDimension(");
assertTrue("loadDimension must exclude an unusable world rather than propagate",
source.substring(loadDimension).contains("catch (IllegalStateException unusableStorage)"));
}
@Test
public void bukkitWorldFilteringRecognizesCurrentCraftBukkitConfiguredStorage() throws Exception {
Path worldContainer = temporaryFolder.newFolder("configured-server").toPath();
@@ -3,9 +3,6 @@ package art.arcane.iris.core;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import org.junit.Test;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
@@ -28,55 +25,6 @@ public class ProgressBossBarToggleTest {
() -> BukkitPlatform.showProgressLane(null, "iris:job", "Working", 0.5D, 4000L)));
}
@Test
public void studioOpenBossBarIsGatedBySettings() throws Exception {
assertGated("core/project/StudioOpenProgressReporter.java");
}
@Test
public void worldCreationAlwaysUsesTheActionBarInsteadOfABossBar() throws Exception {
String source = source("core/tools/WorldCreationProgressReporter.java");
assertFalse(source.contains("isProgressBossBar()"));
assertFalse(source.contains("Bukkit.createBossBar("));
assertTrue(source.contains("WORLD_CREATE_LIFECYCLE_ACTION"));
}
@Test
public void chunkJobBossBarIsGatedBySettings() throws Exception {
assertGated("core/runtime/ChunkJobReporter.java");
}
@Test
public void packDownloadLaneIsGatedWhileTheActionBarSurvives() throws Exception {
String source = source("core/service/PackDownloadProgressReporter.java");
assertTrue(source.contains("isProgressBossBar()"));
assertTrue("the action bar must keep reporting when boss bars are off",
source.contains("sender.sendAction(snapshot.line())"));
}
@Test
public void pregenLaneIsGatedWhileTheActionBarSurvives() throws Exception {
String source = source("core/tools/IrisCreator.java");
int lane = source.indexOf("\"iris:pregen\"");
int gate = source.lastIndexOf("isProgressBossBar()", lane);
assertTrue("the pregen boss bar lane must sit behind general.progressBossBar",
gate > 0 && lane - gate < 200);
assertTrue("the action bar must keep reporting when boss bars are off",
source.contains("RuntimeProgressMessages.WORLD_PREGEN_ACTION"));
}
private void assertGated(String relativePath) throws Exception {
assertTrue(relativePath + " must consult general.progressBossBar before creating a boss bar",
source(relativePath).contains("isProgressBossBar()"));
}
private String source(String relativePath) throws Exception {
return Files.readString(Path.of("src/main/java/art/arcane/iris").resolve(relativePath)).replace("\r\n", "\n");
}
private void withProgressBossBar(boolean enabled, Runnable body) {
IrisSettings previous = IrisSettings.settings;
try {
@@ -384,43 +384,6 @@ public class ServerConfiguratorDatapackFingerprintTest {
}
}
@Test
public void recoveryRunsBeforeRestoredFingerprintReuseHashFallbackAndCompilation() throws Exception {
String source = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/ServerConfigurator.java")).replace("\r\n", "\n");
int installIfChanged = source.indexOf("installDataPacksIfChanged(boolean fullInstall)");
int recovery = source.indexOf("DatapackIngestService.reapplyFromStaging", installIfChanged);
int restored = source.indexOf("restoredCompilerInputFingerprint()", recovery);
int fingerprint = source.indexOf("computeCurrentDatapackCompilerInputFingerprint", restored);
int earlyReturn = source.indexOf("resultForUnchangedFingerprint", fingerprint);
int compile = source.indexOf("compileDataPacksLocked(", earlyReturn);
int cache = source.indexOf("writeCompilerInputFingerprintCache(cacheFile.toPath(), current)", compile);
assertTrue(recovery >= 0);
assertTrue(restored > recovery);
assertTrue(fingerprint > restored);
assertTrue(earlyReturn > fingerprint);
assertTrue(compile > earlyReturn);
assertTrue(cache > compile);
}
@Test
public void studioTimingSeparatesRecoveryFingerprintCompilationAndTotal() throws Exception {
String source = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/ServerConfigurator.java")).replace("\r\n", "\n");
int timedInstall = source.indexOf("BiConsumer<String, Long> timingConsumer");
int recovery = source.indexOf("\"datapack_external_recovery\"", timedInstall);
int fingerprint = source.indexOf("\"datapack_compiler_input_fingerprint\"", recovery);
int compile = source.indexOf("\"datapack_compile_publish\"", fingerprint);
int total = source.indexOf("\"datapack_install_if_changed_total\"", compile);
assertTrue(timedInstall >= 0);
assertTrue(recovery > timedInstall);
assertTrue(fingerprint > recovery);
assertTrue(compile > fingerprint);
assertTrue(total > compile);
}
@Test
public void loadedRuntimeReuseRequiresAnExactPinnedCompilerInputFingerprint() {
assertTrue(ServerConfigurator.reusableRuntimeFingerprint("abc", "abc"));
@@ -9,8 +9,6 @@ import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.junit.Assert.assertFalse;
@@ -92,30 +90,6 @@ public class ServerConfiguratorStartupRestartTest {
assertFalse(directRestartInvocation.get());
}
@Test
public void startupRestartPathsPromoteTheValidationStateAndBypassTickQueues() throws Exception {
String source = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/ServerConfigurator.java"));
String configure = section(
source,
"public static void configure()",
"public static boolean isLoadedDatapackRuntimeReady");
String startupRestart = section(
source,
"public static void restartAtStartupBoundary",
"public static boolean verifyDataPackInstalled");
int restartResult = configure.indexOf("if (result.restartRequired())");
int validationRestart = configure.indexOf("requireDatapackRestart();", restartResult);
assertTrue(restartResult >= 0);
assertTrue(validationRestart > restartResult);
assertTrue(startupRestart.indexOf("invokeImmediateRestartIfSupported(Bukkit.class)")
< startupRestart.indexOf("Bukkit.shutdown();"));
assertFalse(startupRestart.contains("Bukkit.restart"));
assertFalse(startupRestart.contains("J.s("));
assertFalse(startupRestart.contains("dispatchCommand"));
}
public static final class RestartCapableApi {
private static boolean restarted;
@@ -126,12 +100,4 @@ public class ServerConfiguratorStartupRestartTest {
public static final class ShutdownOnlyApi {
}
private static String section(String source, String startMarker, String endMarker) {
int start = source.indexOf(startMarker);
int end = source.indexOf(endMarker, start);
assertTrue(start >= 0);
assertTrue(end > start);
return source.substring(start, end);
}
}
@@ -2,8 +2,10 @@ package art.arcane.iris.core;
import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.core.localization.IrisMessages;
import art.arcane.iris.testsupport.Await;
import art.arcane.volmlib.util.hotload.ConfigHotloadEngine;
import art.arcane.volmlib.util.localization.MessageArgument;
import java.time.Duration;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
@@ -15,7 +17,6 @@ import java.io.File;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.FileTime;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@@ -214,12 +215,12 @@ public class SettingsHotloadWatchTest {
assertEquals(1, automaticApplies.get());
Files.writeString(override.toPath(), locale("en_US", "Manual {permission}"), StandardCharsets.UTF_8);
pollFor(125L);
pollWithoutChange(125L, "First " + PERMISSION);
assertEquals("First " + PERMISSION, permissionMessage());
assertTrue(IrisLanguage.reload(dataFolder, "en_US"));
assertEquals("Manual " + PERMISSION, permissionMessage());
pollFor(650L);
pollWithoutChange(650L, "Manual " + PERMISSION);
assertEquals(1, automaticApplies.get());
assertEquals("Manual " + PERMISSION, permissionMessage());
}
@@ -255,32 +256,11 @@ public class SettingsHotloadWatchTest {
assertEquals("Active " + PERMISSION, permissionMessage());
}
@Test
public void bukkitAndModdedUseTheSameCoreCoordinator() throws Exception {
String bukkit = Files.readString(Path.of(
"../adapters/bukkit/plugin/src/main/java/art/arcane/iris/Iris.java"
));
String modded = Files.readString(Path.of(
"../adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedSettingsHotloadService.java"
));
assertTrue(bukkit.contains("new SettingsHotloadWatch("));
assertTrue(modded.contains("new SettingsHotloadWatch("));
assertFalse(bukkit.contains("new ConfigHotloadEngine("));
assertFalse(modded.contains("new ConfigHotloadEngine("));
assertFalse(bukkit.contains("IrisLanguage.update()"));
assertFalse(modded.contains("IrisLanguage.update()"));
}
private void awaitPermissionMessage(String expected) throws Exception {
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(6L);
while (System.nanoTime() < deadline) {
private void awaitPermissionMessage(String expected) {
Await.reached("the hotload permission message '" + expected + "'", Duration.ofSeconds(6L), () -> {
watch.checkConfigHotload();
if (expected.equals(permissionMessage())) {
return;
}
Thread.sleep(25L);
}
return expected.equals(permissionMessage());
});
assertEquals(expected, permissionMessage());
}
@@ -293,31 +273,24 @@ public class SettingsHotloadWatchTest {
}
private void checkUntilEntered(CountDownLatch entered) {
try {
while (entered.getCount() > 0L) {
watch.checkConfigHotload();
Thread.sleep(10L);
}
} catch (InterruptedException failure) {
Thread.currentThread().interrupt();
throw new IllegalStateException("Interrupted while waiting for a hotload snapshot", failure);
}
Await.until("the hotload apply boundary", Duration.ofSeconds(5L), () -> {
watch.checkConfigHotload();
return entered.getCount() == 0L;
});
}
private void awaitThreadState(Thread thread, Thread.State expected) throws InterruptedException {
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(1L);
while (System.nanoTime() < deadline && thread.getState() != expected) {
Thread.sleep(5L);
}
private void awaitThreadState(Thread thread, Thread.State expected) {
Await.reached(thread.getName() + " to reach " + expected, Duration.ofSeconds(1L),
() -> thread.getState() == expected);
assertEquals(expected, thread.getState());
}
private void pollFor(long durationMillis) throws InterruptedException {
long deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(durationMillis);
while (System.nanoTime() < deadline) {
watch.checkConfigHotload();
Thread.sleep(25L);
}
private void pollWithoutChange(long windowMillis, String expected) {
assertFalse("The hotload permission message changed inside the quiet window",
Await.reached("an unexpected hotload apply", Duration.ofMillis(windowMillis), () -> {
watch.checkConfigHotload();
return !expected.equals(permissionMessage());
}));
}
private String captureErrorsWhilePolling(String expected, long duplicateWindowMillis) throws Exception {
@@ -325,20 +298,17 @@ public class SettingsHotloadWatchTest {
ByteArrayOutputStream captured = new ByteArrayOutputStream();
try (PrintStream capture = new PrintStream(captured, true, StandardCharsets.UTF_8)) {
System.setErr(capture);
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(4L);
while (!captured.toString(StandardCharsets.UTF_8).contains(expected)) {
if (System.nanoTime() >= deadline) {
fail("Timed out after 4000ms waiting for the hotload diagnostic '" + expected
+ "'; captured stderr was: " + captured.toString(StandardCharsets.UTF_8));
}
if (!Await.reached("the hotload diagnostic '" + expected + "'", Duration.ofSeconds(4L), () -> {
watch.checkConfigHotload();
Thread.sleep(10L);
return captured.toString(StandardCharsets.UTF_8).contains(expected);
})) {
fail("Timed out after 4000ms waiting for the hotload diagnostic '" + expected
+ "'; captured stderr was: " + captured.toString(StandardCharsets.UTF_8));
}
long duplicateDeadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(duplicateWindowMillis);
while (System.nanoTime() < duplicateDeadline) {
Await.reached("a duplicate hotload diagnostic", Duration.ofMillis(duplicateWindowMillis), () -> {
watch.checkConfigHotload();
Thread.sleep(10L);
}
return false;
});
} finally {
System.setErr(originalError);
}
@@ -1,51 +0,0 @@
package art.arcane.iris.core.datapack;
import org.junit.Test;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertTrue;
public class DatapackIngestRuntimeInvalidationTest {
@Test
public void everyPublicMutationInvalidatesLoadedRuntimeBeforeLocking() throws Exception {
String source = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/datapack/DatapackIngestService.java")).replace("\r\n", "\n");
assertInvalidatesBeforeLock(source, "public static Report ingest(");
assertInvalidatesBeforeLock(source, "public static ReapplyOutcome reapplyFromStaging(");
assertInvalidatesBeforeLock(source, "public static boolean remove(");
}
@Test
public void noChangeChecksRestoreWhileChangedIngestRemainsRestartBound() throws Exception {
String source = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/datapack/DatapackIngestService.java")).replace("\r\n", "\n");
int ingest = source.indexOf("public static Report ingest(");
int unlock = source.indexOf("TRANSACTION_LOCK.unlock();", ingest);
int restore = source.indexOf(
"ServerConfigurator.restoreLoadedDatapackRuntimeIfUnchanged(invalidation);",
unlock);
int changed = source.indexOf("else if (report.changed())", restore);
int restart = source.indexOf("ServerConfigurator.restart();", changed);
int restartRequired = source.indexOf("ServerConfigurator.requireDatapackRestart();", restart);
assertTrue(ingest >= 0);
assertTrue(unlock > ingest);
assertTrue(restore > unlock);
assertTrue(changed > restore);
assertTrue(restart > changed);
assertTrue(restartRequired > restart);
}
private void assertInvalidatesBeforeLock(String source, String signature) {
int method = source.indexOf(signature);
int invalidation = source.indexOf("ServerConfigurator.invalidateLoadedDatapackRuntime();", method);
int lock = source.indexOf("TRANSACTION_LOCK.lock();", method);
assertTrue("Missing datapack mutation entry point " + signature, method >= 0);
assertTrue("Loaded runtime must be invalidated before " + signature, invalidation > method);
assertTrue("Loaded runtime invalidation must precede the transaction lock", invalidation < lock);
}
}
File diff suppressed because it is too large. Load diff
@@ -3,7 +3,9 @@ package art.arcane.iris.core.gui;
import art.arcane.iris.engine.framework.MeteredCache;
import art.arcane.iris.engine.framework.PreservationRegistry;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.testsupport.PlatformLeakGuard;
import art.arcane.volmlib.util.function.NoiseProvider;
import org.junit.ClassRule;
import org.junit.Test;
import javax.swing.SwingUtilities;
@@ -20,6 +22,9 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public final class NoiseExplorerGUITest {
@ClassRule
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
@Test
public void openingInitialSourceBuildsSamplerExactlyOnce() throws Exception {
AtomicInteger factoryCalls = new AtomicInteger();
@@ -133,6 +133,8 @@ public final class NoiseRenderCoordinatorTest {
AtomicReference<Throwable> failure = new AtomicReference<>();
AtomicReference<String> samplingThread = new AtomicReference<>();
AtomicBoolean sampledOnEdt = new AtomicBoolean();
AtomicBoolean renderFinished = new AtomicBoolean();
CountDownLatch sampledAfterCompletion = new CountDownLatch(1);
AtomicInteger samples = new AtomicInteger();
NoiseRenderCoordinator coordinator = new NoiseRenderCoordinator(2, new NoiseRenderCoordinator.Listener() {
@Override
@@ -154,6 +156,9 @@ public final class NoiseRenderCoordinatorTest {
try {
NoiseProvider sampler = (x, z) -> {
samples.incrementAndGet();
if (renderFinished.get()) {
sampledAfterCompletion.countDown();
}
samplingThread.compareAndSet(null, Thread.currentThread().getName());
sampledOnEdt.compareAndSet(false, SwingUtilities.isEventDispatchThread());
return x + z;
@@ -177,7 +182,8 @@ public final class NoiseRenderCoordinatorTest {
assertFalse(sampledOnEdt.get());
assertTrue(samplingThread.get().startsWith("Iris Noise Renderer"));
int completedSamples = samples.get();
Thread.sleep(150L);
renderFinished.set(true);
assertFalse(sampledAfterCompletion.await(150L, TimeUnit.MILLISECONDS));
assertEquals(completedSamples, samples.get());
} finally {
coordinator.close();
@@ -8,6 +8,7 @@ import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.testsupport.PlatformLeakGuard;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.format.MemoryMonitor;
import art.arcane.volmlib.util.scheduling.FoliaScheduler;
@@ -17,6 +18,7 @@ import org.bukkit.plugin.IllegalPluginAccessException;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.PluginManager;
import org.bukkit.scheduler.BukkitScheduler;
import org.junit.ClassRule;
import org.junit.Test;
import org.mockito.MockedStatic;
@@ -45,6 +47,9 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class PregeneratorJobShutdownDispatchTest {
@ClassRule
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
@Test
public void pluginDisableCanJoinAWorkerAlreadyDispatchingSaving() throws Exception {
try (Fixture fixture = new Fixture()) {
@@ -2,9 +2,11 @@ package art.arcane.iris.core.gui;
import art.arcane.iris.engine.framework.render.IrisRenderer;
import art.arcane.iris.engine.framework.render.RenderType;
import art.arcane.iris.testsupport.Await;
import org.junit.Test;
import java.awt.image.BufferedImage;
import java.time.Duration;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -86,7 +88,8 @@ public class VisionRenderControllerTest {
VisionRenderController.RenderSpec spec = spec(renderer, RenderType.BIOME, 1L, 0D, 0D, 4D, 128, 128);
try {
VisionRenderController.Frame first = controller.request(spec);
assertTrue(await(() -> controller.progress(first).ready() == controller.progress(first).total(), 2_000L));
Await.until("the first render to report every tile ready", Duration.ofSeconds(2L),
() -> controller.progress(first).ready() == controller.progress(first).total());
int rendered = calls.get();
VisionRenderController.Frame cached = controller.request(spec);
@@ -108,7 +111,8 @@ public class VisionRenderControllerTest {
VisionRenderController controller = controller(2);
try {
VisionRenderController.Frame first = controller.request(spec(renderer, RenderType.BIOME, 1L, 16D, 16D, 4D, 128, 128));
assertTrue(await(() -> controller.progress(first).ready() == controller.progress(first).total(), 2_000L));
Await.until("the first render to report every tile ready", Duration.ofSeconds(2L),
() -> controller.progress(first).ready() == controller.progress(first).total());
Set<VisionRenderController.TileKey> firstKeys = keys(first);
VisionRenderController.Frame panned = controller.request(spec(renderer, RenderType.BIOME, 1L, 20D, 20D, 4D, 128, 128));
@@ -249,15 +253,4 @@ public class VisionRenderControllerTest {
height
);
}
private static boolean await(BooleanSupplier condition, long timeoutMillis) throws InterruptedException {
long deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(timeoutMillis);
while (System.nanoTime() < deadline) {
if (condition.getAsBoolean()) {
return true;
}
Thread.sleep(5L);
}
return condition.getAsBoolean();
}
}
@@ -0,0 +1,51 @@
package art.arcane.iris.core.lifecycle;
import art.arcane.iris.core.runtime.WorldRuntimeControlService;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.testsupport.BukkitTestServer;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertNull;
/**
* Iris keeps process-wide caches that describe the running server: the service registry, and the lifecycle
* and runtime-control singletons that each hold a capability snapshot of reflective handles onto the live
* server. Nothing cleared them on disable, so a re-enable in the same JVM kept answering from the previous
* boot's snapshot.
*/
public class IrisRuntimeStaticsTest {
@Before
public void installServer() {
BukkitTestServer.install();
}
@After
public void clearStatics() {
IrisRuntimeStatics.reset();
}
@Test
public void resetUnbindsEveryRegisteredService() {
Runnable service = () -> {
};
IrisServices.register(Runnable.class, service);
IrisRuntimeStatics.reset();
assertNull(IrisServices.getOrNull(Runnable.class));
}
@Test
public void resetDropsTheCachedCapabilitySingletons() {
WorldLifecycleService lifecycle = WorldLifecycleService.get();
WorldRuntimeControlService control = WorldRuntimeControlService.get();
IrisRuntimeStatics.reset();
assertNotSame(lifecycle, WorldLifecycleService.get());
assertNotSame(control, WorldRuntimeControlService.get());
}
}
@@ -3,8 +3,10 @@ package art.arcane.iris.core.lifecycle;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.LogLevel;
import art.arcane.iris.testsupport.PlatformLeakGuard;
import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
import java.nio.file.Path;
@@ -22,6 +24,9 @@ import static org.mockito.Mockito.mock;
* console and the runtime log but never the instance's logs/latest.log - the only log most operators read.
*/
public class MissingWorldStorageLogTest {
@ClassRule
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
private final List<String> emitted = new ArrayList<>();
private final List<LogLevel> levels = new ArrayList<>();
private IrisPlatform previousPlatform;
@@ -0,0 +1,66 @@
package art.arcane.iris.core.lifecycle;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
/**
* A level storage access that will not close still holds the world's session lock, and the next attempt to
* open that world fails with a lock message that names no cause. The close failure used to be swallowed
* whole, so the console had nothing to connect the two.
*/
public class WorldLifecycleSupportCloseFailureTest {
private IrisPlatform capturingPlatform;
private IrisPlatform previousPlatform;
@Before
public void captureReports() {
previousPlatform = IrisPlatforms.isBound() ? IrisPlatforms.get() : null;
IrisPlatforms.unbind();
capturingPlatform = mock(IrisPlatform.class);
IrisPlatforms.bind(capturingPlatform);
}
@After
public void restorePlatform() {
IrisPlatforms.unbind();
if (previousPlatform != null) {
IrisPlatforms.bind(previousPlatform);
}
}
@Test
public void aFailedStorageCloseIsReportedWithItsCause() {
WorldLifecycleSupport.closeLevelStorageAccess(new FailingStorageAccess());
ArgumentCaptor<Throwable> reported = ArgumentCaptor.forClass(Throwable.class);
verify(capturingPlatform).reportError(anyString(), reported.capture());
assertEquals("session lock still held", reported.getValue().getMessage());
}
@Test
public void aClosableStorageAccessReportsNothing() {
WorldLifecycleSupport.closeLevelStorageAccess(new ClosableStorageAccess());
verify(capturingPlatform, org.mockito.Mockito.never()).reportError(anyString(), org.mockito.ArgumentMatchers.any());
}
public static final class FailingStorageAccess {
public void close() {
throw new IllegalStateException("session lock still held");
}
}
public static final class ClosableStorageAccess {
public void close() {
}
}
}
@@ -10,8 +10,6 @@ import org.junit.Test;
import org.mockito.MockedStatic;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.CompletionStage;
@@ -59,40 +57,6 @@ public class WorldLifecycleUnloadAsyncTest {
}
}
@Test
public void paperLikeManualUnloadDetachesBeforeOffThreadChunkDrain() throws Exception {
String source = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/lifecycle/WorldLifecycleSupport.java"))
.replace("\r\n", "\n");
int unloadStart = source.indexOf("private static CompletableFuture<Boolean> unloadWorldWithoutAsyncApi(");
int unloadEnd = source.indexOf("static boolean announceManualWorldUnload", unloadStart);
String unloadMethod = source.substring(unloadStart, unloadEnd);
int capabilityFallback = unloadMethod.indexOf(
"capabilities.minecraftServer() == null || capabilities.removeLevelMethod() == null");
int bukkitUnload = unloadMethod.indexOf("Bukkit.unloadWorld(world, save)", capabilityFallback);
int detach = unloadMethod.indexOf("detachServerLevelAsync(capabilities, serverLevel, world)");
int drain = unloadMethod.indexOf("drainChunkTasksAsync(world, serverLevel)");
int close = unloadMethod.indexOf("closeServerLevelAsync(world, serverLevel)");
int closeStart = source.indexOf("private static CompletableFuture<Void> closeServerLevelAsync(");
int closeEnd = source.indexOf("private static CompletableFuture<Void> drainChunkTasksAsync(", closeStart);
String closeMethod = source.substring(closeStart, closeEnd);
int drainStart = closeEnd;
int drainEnd = source.indexOf("private static void removeWorldFromCraftServerMap", drainStart);
String drainMethod = source.substring(drainStart, drainEnd);
assertTrue(capabilityFallback >= 0);
assertTrue(bukkitUnload > capabilityFallback);
assertTrue(detach > bukkitUnload);
assertTrue(drain > detach);
assertTrue(close > drain);
assertTrue(drainMethod.contains("return J.afut(() ->"));
assertTrue(drainMethod.contains("\"moonrise$getChunkTaskScheduler\""));
assertTrue(drainMethod.contains("\"halt\""));
assertTrue(closeMethod.contains("return runGlobalAsync(closeTask)"));
assertFalse(closeMethod.contains("J.afut("));
assertFalse(closeMethod.contains("J.runRegion("));
}
@Test
public void reflectedAsyncUnloadWaitsForTrueCallback() throws Exception {
CallbackServer server = new CallbackServer();
@@ -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");
@@ -0,0 +1,48 @@
package art.arcane.iris.core.link;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class IdentifierTest {
@Test
public void abareKeyIsQualifiedIntoTheMinecraftNamespace() {
Identifier identifier = Identifier.fromString("stone");
assertEquals("minecraft", identifier.namespace());
assertEquals("stone", identifier.key());
}
@Test
public void aQualifiedKeyKeepsBothHalves() {
Identifier identifier = Identifier.fromString("oraxen:ruby_ore");
assertEquals("oraxen", identifier.namespace());
assertEquals("ruby_ore", identifier.key());
}
@Test
public void onlyTheFirstSeparatorSplitsTheIdentifier() {
Identifier identifier = Identifier.fromString("itemsadder:rocks/ruby:ore");
assertEquals("itemsadder", identifier.namespace());
assertEquals("rocks/ruby:ore", identifier.key());
}
@Test
public void aLeadingSeparatorProducesAnEmptyNamespaceRatherThanADefault() {
Identifier identifier = Identifier.fromString(":ruby_ore");
assertEquals("", identifier.namespace());
assertEquals("ruby_ore", identifier.key());
}
@Test
public void theRenderedFormRoundTripsThroughTheParser() {
Identifier identifier = new Identifier("nexo", "village/lamp");
assertEquals("nexo:village/lamp", identifier.toString());
assertEquals(identifier, Identifier.fromString(identifier.toString()));
assertEquals(Identifier.fromString("stone"), Identifier.fromString("minecraft:stone"));
}
}
@@ -18,8 +18,6 @@ import org.mvplugins.multiverse.external.vavr.control.Option;
import java.io.File;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
@@ -53,16 +51,6 @@ public class MultiverseCoreLinkTest {
&& method.getParameterCount() == 2));
}
@Test
public void registrationDoesNotRewriteTheRecordedMultiverseName() throws Exception {
String source = Files.readString(Path.of("src/main/java/art/arcane/iris/core/link/MultiverseCoreLink.java")).replace("\r\n", "\n");
assertFalse("worlds are created under the startup name, so the recorded name needs no correction",
source.contains("setLegacyWorldName"));
assertTrue("Multiverse still has to be told the live world name it will record",
source.contains("ImportWorldOptions.worldName(world.getName())"));
}
@Test
public void lookupNamesAddTheIrisKeyAndRuntimeNameForConfiguredStartupNames() {
assertEquals(
@@ -3,12 +3,14 @@ package art.arcane.iris.core.link;
import art.arcane.iris.core.lifecycle.ManagedWorldLoader;
import art.arcane.iris.engine.platform.BukkitChunkGenerator;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.testsupport.PlatformLeakGuard;
import org.bukkit.Bukkit;
import org.bukkit.Server;
import org.bukkit.World;
import org.bukkit.command.ConsoleCommandSender;
import org.bukkit.event.server.ServerCommandEvent;
import org.bukkit.plugin.PluginManager;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
@@ -29,6 +31,9 @@ import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
public class MultiverseGuardListenerTest {
@ClassRule
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@@ -0,0 +1,156 @@
package art.arcane.iris.core.link.data;
import art.arcane.iris.core.link.ExternalDataProvider;
import art.arcane.iris.core.link.Identifier;
import art.arcane.volmlib.util.collection.KMap;
import org.bukkit.block.data.BlockData;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.MissingResourceException;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
public class DataTypeProbeContractTest {
private static final Identifier RUBY = new Identifier("testplugin", "ruby_ore");
@Test
public void anUnclaimedIdentifierIsRejectedWithoutProbingTheProvider() {
RecordingProvider provider = new RecordingProvider(false, true, true);
assertFalse(DataType.BLOCK.test(provider, RUBY));
assertFalse(DataType.ITEM.test(provider, RUBY));
assertFalse(DataType.ENTITY.test(provider, RUBY));
assertEquals(List.of(), provider.probes);
}
@Test
public void aClaimedBlockIsProbedThroughTheStatelessBlockLookup() {
RecordingProvider provider = new RecordingProvider(true, true, true);
assertTrue(DataType.BLOCK.test(provider, RUBY));
assertEquals(List.of("block"), provider.probes);
}
@Test
public void aClaimedItemIsProbedThroughTheStatelessItemLookup() {
RecordingProvider provider = new RecordingProvider(true, true, true);
assertTrue(DataType.ITEM.test(provider, RUBY));
assertEquals(List.of("item"), provider.probes);
}
@Test
public void aClaimedEntityIsAcceptedOnTheClaimAlone() {
RecordingProvider provider = new RecordingProvider(true, false, false);
assertTrue(DataType.ENTITY.test(provider, RUBY));
assertEquals(List.of(), provider.probes);
}
@Test
public void aMissingResourceDowngradesTheClaimInsteadOfFailing() {
RecordingProvider provider = new RecordingProvider(true, false, false);
assertFalse(DataType.BLOCK.test(provider, RUBY));
assertFalse(DataType.ITEM.test(provider, RUBY));
assertEquals(List.of("block", "item"), provider.probes);
}
@Test
public void anUnexpectedProviderFailureIsNotSwallowed() {
ExplodingProvider provider = new ExplodingProvider();
assertThrows(IllegalStateException.class, () -> DataType.BLOCK.test(provider, RUBY));
}
@Test
public void thePredicateFormFiltersEveryElementThroughTheSameProvider() {
RecordingProvider provider = new RecordingProvider(true, true, true);
List<Identifier> claimed = List.of(RUBY, new Identifier("other", "quartz"));
List<Identifier> filtered = claimed.stream().filter(DataType.BLOCK.asPredicate(provider)).toList();
assertEquals(claimed, filtered);
assertEquals(List.of("block", "block"), provider.probes);
}
private static final class RecordingProvider extends ExternalDataProvider {
private final List<String> probes = new ArrayList<>();
private final boolean claims;
private final boolean resolvesBlocks;
private final boolean resolvesItems;
private RecordingProvider(boolean claims, boolean resolvesBlocks, boolean resolvesItems) {
super("TestPlugin");
this.claims = claims;
this.resolvesBlocks = resolvesBlocks;
this.resolvesItems = resolvesItems;
}
@Override
public void init() {
}
@Override
public @NotNull BlockData getBlockData(@NotNull Identifier blockId, @NotNull KMap<String, String> state) {
probes.add("block");
if (!resolvesBlocks) {
throw new MissingResourceException("no block", blockId.namespace(), blockId.key());
}
return mock(BlockData.class);
}
@Override
public @NotNull ItemStack getItemStack(@NotNull Identifier itemId, @NotNull KMap<String, Object> customNbt) {
probes.add("item");
if (!resolvesItems) {
throw new MissingResourceException("no item", itemId.namespace(), itemId.key());
}
return mock(ItemStack.class);
}
@Override
public @NotNull Collection<@NotNull Identifier> getTypes(@NotNull DataType dataType) {
return List.of();
}
@Override
public boolean isValidProvider(@NotNull Identifier id, DataType dataType) {
return claims;
}
}
private static final class ExplodingProvider extends ExternalDataProvider {
private ExplodingProvider() {
super("TestPlugin");
}
@Override
public void init() {
}
@Override
public @NotNull BlockData getBlockData(@NotNull Identifier blockId, @NotNull KMap<String, String> state) {
throw new IllegalStateException("the provider backend is broken");
}
@Override
public @NotNull Collection<@NotNull Identifier> getTypes(@NotNull DataType dataType) {
return List.of();
}
@Override
public boolean isValidProvider(@NotNull Identifier id, DataType dataType) {
return true;
}
}
}
@@ -0,0 +1,110 @@
package art.arcane.iris.core.link.data;
import art.arcane.iris.core.link.ExternalDataProvider;
import art.arcane.iris.core.link.Identifier;
import art.arcane.volmlib.util.collection.KMap;
import org.bukkit.Location;
import org.bukkit.block.data.BlockData;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.MissingResourceException;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
public class ExternalDataProviderDefaultsTest {
private static final Identifier RUBY = new Identifier("testplugin", "ruby_ore");
@Test
public void theProviderRemembersThePluginItSpeaksFor() {
assertEquals("TestPlugin", new StatelessProvider().getPluginId());
}
@Test
public void aProviderWithoutAPluginIdIsRejected() {
assertThrows(NullPointerException.class, () -> new StatelessProvider(null));
}
@Test
public void anUnimplementedProviderRefusesEveryResourceKind() {
StatelessProvider provider = new StatelessProvider();
MissingResourceException blockFailure =
assertThrows(MissingResourceException.class, () -> provider.getBlockData(RUBY));
MissingResourceException itemFailure =
assertThrows(MissingResourceException.class, () -> provider.getItemStack(RUBY));
assertEquals("testplugin", blockFailure.getClassName());
assertEquals("ruby_ore", blockFailure.getKey());
assertEquals("testplugin", itemFailure.getClassName());
assertEquals("ruby_ore", itemFailure.getKey());
assertThrows(MissingResourceException.class, () -> provider.spawnMob(mock(Location.class), RUBY));
}
@Test
public void anUnimplementedProviderExposesNoBlockProperties() {
assertTrue(new StatelessProvider().getBlockProperties(RUBY).isEmpty());
}
@Test
public void theStatelessLookupsDelegateWithAnEmptyStateMap() {
DelegationProvider provider = new DelegationProvider();
provider.getBlockData(RUBY);
provider.getItemStack(RUBY);
assertEquals(List.of("block:0", "item:0"), provider.calls);
}
@Test
public void deferredPlacementIsANoOpUntilAProviderImplementsIt() {
new StatelessProvider().processUpdate(null, null, RUBY);
}
private static class StatelessProvider extends ExternalDataProvider {
private StatelessProvider() {
super("TestPlugin");
}
private StatelessProvider(String pluginId) {
super(pluginId);
}
@Override
public void init() {
}
@Override
public @NotNull Collection<@NotNull Identifier> getTypes(@NotNull DataType dataType) {
return List.of();
}
@Override
public boolean isValidProvider(@NotNull Identifier id, DataType dataType) {
return false;
}
}
private static final class DelegationProvider extends StatelessProvider {
private final List<String> calls = new ArrayList<>();
@Override
public @NotNull BlockData getBlockData(@NotNull Identifier blockId, @NotNull KMap<String, String> state) {
calls.add("block:" + state.size());
return mock(BlockData.class);
}
@Override
public @NotNull ItemStack getItemStack(@NotNull Identifier itemId, @NotNull KMap<String, Object> customNbt) {
calls.add("item:" + customNbt.size());
return mock(ItemStack.class);
}
}
}
@@ -0,0 +1,90 @@
package art.arcane.iris.core.link.data;
import art.arcane.iris.core.link.Identifier;
import org.junit.Test;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class ProviderNamespaceClaimTest {
@Test
public void ecoItemsClaimsOnlyItemsInItsOwnNamespace() {
EcoItemsDataProvider provider = new EcoItemsDataProvider();
assertTrue(provider.isValidProvider(new Identifier("ecoitems", "ruby"), DataType.ITEM));
assertTrue(provider.isValidProvider(new Identifier("EcoItems", "ruby"), DataType.ITEM));
assertFalse(provider.isValidProvider(new Identifier("ecoitems", "ruby"), DataType.BLOCK));
assertFalse(provider.isValidProvider(new Identifier("ecoitems", "ruby"), DataType.ENTITY));
assertFalse(provider.isValidProvider(new Identifier("oraxen", "ruby"), DataType.ITEM));
}
@Test
public void executableItemsClaimsOnlyItemsInItsOwnNamespace() {
ExecutableItemsDataProvider provider = new ExecutableItemsDataProvider();
assertTrue(provider.isValidProvider(new Identifier("executable_items", "wand"), DataType.ITEM));
assertFalse(provider.isValidProvider(new Identifier("executable_items", "wand"), DataType.BLOCK));
assertFalse(provider.isValidProvider(new Identifier("executableitems", "wand"), DataType.ITEM));
}
@Test
public void kgeneratorsClaimsItemsWithoutConsultingTheGeneratorRegistry() {
KGeneratorsDataProvider provider = new KGeneratorsDataProvider();
assertTrue(provider.isValidProvider(new Identifier("kgenerators", "cobble"), DataType.ITEM));
assertFalse(provider.isValidProvider(new Identifier("kgenerators", "cobble"), DataType.ENTITY));
assertFalse(provider.isValidProvider(new Identifier("oraxen", "cobble"), DataType.ITEM));
}
@Test
public void mmoItemsEncodesItsItemTypeInTheNamespace() {
MMOItemsDataProvider provider = new MMOItemsDataProvider();
assertTrue(provider.isValidProvider(new Identifier("mmoitems_sword", "excalibur"), DataType.ITEM));
assertFalse(provider.isValidProvider(new Identifier("mmoitems", "excalibur"), DataType.ITEM));
assertFalse(provider.isValidProvider(new Identifier("mmoitems_sword", "excalibur"), DataType.ENTITY));
assertFalse(provider.isValidProvider(new Identifier("MMOITEMS", "1"), DataType.BLOCK));
}
@Test
public void mythicCrucibleClaimsItemsWithoutConsultingItsItemManager() {
MythicCrucibleDataProvider provider = new MythicCrucibleDataProvider();
assertTrue(provider.isValidProvider(new Identifier("crucible", "ruby"), DataType.ITEM));
assertTrue(provider.isValidProvider(new Identifier("Crucible", "ruby"), DataType.ITEM));
assertFalse(provider.isValidProvider(new Identifier("crucible", "ruby"), DataType.ENTITY));
assertFalse(provider.isValidProvider(new Identifier("oraxen", "ruby"), DataType.ITEM));
}
@Test
public void oraxenRefusesForeignNamespacesBeforeTouchingItsApi() {
OraxenDataProvider provider = new OraxenDataProvider();
assertFalse(provider.isValidProvider(new Identifier("nexo", "ruby_ore"), DataType.BLOCK));
assertFalse(provider.isValidProvider(new Identifier("nexo", "ruby_ore"), DataType.ITEM));
assertEquals(List.of(), provider.getTypes(DataType.ENTITY));
}
@Test
public void nexoRefusesForeignNamespacesBeforeTouchingItsApi() {
NexoDataProvider provider = new NexoDataProvider();
assertFalse(provider.isValidProvider(new Identifier("oraxen", "lamp"), DataType.BLOCK));
assertFalse(provider.isValidProvider(new Identifier("oraxen", "lamp"), DataType.ITEM));
assertEquals(List.of(), provider.getTypes(DataType.ENTITY));
}
@Test
public void itemsAdderClaimsNothingBeforeItsRegistryHasLoaded() {
ItemAdderDataProvider provider = new ItemAdderDataProvider();
assertFalse(provider.isValidProvider(new Identifier("rocks", "ruby_ore"), DataType.ITEM));
assertFalse(provider.isValidProvider(new Identifier("rocks", "ruby_ore"), DataType.BLOCK));
assertFalse(provider.isValidProvider(new Identifier("rocks", "ruby_ore"), DataType.ENTITY));
assertTrue(provider.getTypes(DataType.ITEM).isEmpty());
assertTrue(provider.getTypes(DataType.BLOCK).isEmpty());
}
}
@@ -4,8 +4,10 @@ import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.engine.framework.PreservationRegistry;
import art.arcane.iris.engine.object.IrisGenerator;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.testsupport.PlatformLeakGuard;
import org.junit.After;
import org.junit.Before;
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.assertTrue;
import static org.mockito.Mockito.mock;
public class IrisDataAuthoringCacheTest {
@ClassRule
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
@Rule
public final TemporaryFolder temporary = new TemporaryFolder();
@@ -3,8 +3,10 @@ package art.arcane.iris.core.loader;
import art.arcane.iris.engine.framework.MeteredCache;
import art.arcane.iris.engine.framework.PreservationRegistry;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.testsupport.PlatformLeakGuard;
import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
@@ -17,6 +19,9 @@ import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
public class IrisDataDatapackCompilerCacheTest {
@ClassRule
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@@ -4,9 +4,11 @@ import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.MeteredCache;
import art.arcane.iris.engine.framework.PreservationRegistry;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.testsupport.PlatformLeakGuard;
import art.arcane.iris.util.project.context.IrisContext;
import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
@@ -21,6 +23,9 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class IrisDataEngineRegistryTest {
@ClassRule
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@@ -5,8 +5,10 @@ import art.arcane.iris.engine.history.GenerationRegistryContract;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.testsupport.PlatformLeakGuard;
import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
@@ -20,6 +22,9 @@ import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.spy;
public class IrisDataRetainedPackTest {
@ClassRule
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@@ -7,10 +7,12 @@ import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.testsupport.PlatformLeakGuard;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.collection.KSet;
import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
@@ -34,6 +36,9 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class ResourceLoaderPrefetchTest {
@ClassRule
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@@ -5,9 +5,11 @@ import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisPosition;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.testsupport.PlatformLeakGuard;
import com.google.gson.Gson;
import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
@@ -26,6 +28,9 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class ResourceLoaderStaticObjectValidationTest {
@ClassRule
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@@ -1,5 +1,6 @@
package art.arcane.iris.core.localization;
import art.arcane.iris.testsupport.ProjectPaths;
import art.arcane.volmlib.util.localization.LocaleOverlay;
import art.arcane.volmlib.util.localization.LocalizationValidator;
import art.arcane.volmlib.util.localization.RemoteLanguageCatalog;
@@ -111,7 +112,7 @@ public class IrisDownloadValidationTest {
}
private String completeCatalog() throws IOException {
return Files.readString(Path.of("src/main/resources/languages", LOCALE + ".json"));
return Files.readString(ProjectPaths.moduleFile("src/main/resources/languages").resolve(LOCALE + ".json"));
}
private String incompleteCatalog(String complete) {
@@ -1,5 +1,6 @@
package art.arcane.iris.core.localization;
import art.arcane.iris.testsupport.ProjectPaths;
import art.arcane.volmlib.util.localization.LocalizationCandidate;
import art.arcane.volmlib.util.localization.LocalizationSnapshot;
import art.arcane.volmlib.util.localization.PluginLanguageEditor;
@@ -49,7 +50,7 @@ public class IrisLanguageEditorTest {
assertTrue(IrisLanguage.reload(directory, "en_US"));
Path cache = IrisLanguage.remote(directory).cacheFile("fr_FR");
Files.createDirectories(cache.getParent());
Files.copy(Path.of("src/main/resources/languages/fr_FR.json"), cache);
Files.copy(ProjectPaths.moduleFile("src/main/resources/languages/fr_FR.json"), cache);
PluginLanguageEditor.Options options = IrisLanguage.editorOptions();
LocalizationSnapshot english = LocalizationSnapshot.create(
LocalizationCandidate.english(IrisLanguage.catalog(), PluralSelector.oneOther()));
@@ -106,7 +107,7 @@ public class IrisLanguageEditorTest {
@Test
public void incompleteDownloadedLocaleCanBeEditedWithoutBeingSelected() throws Exception {
byte[] complete = Files.readAllBytes(Path.of("src/main/resources/languages/fr_FR.json"));
byte[] complete = Files.readAllBytes(ProjectPaths.moduleFile("src/main/resources/languages/fr_FR.json"));
AtomicInteger requests = new AtomicInteger();
Field remoteField = IrisLanguage.class.getDeclaredField("remote");
remoteField.setAccessible(true);
@@ -1,6 +1,7 @@
package art.arcane.iris.core.localization;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.testsupport.ProjectPaths;
import art.arcane.volmlib.util.localization.MessageArgument;
import art.arcane.volmlib.util.localization.LocaleOverlay;
import art.arcane.volmlib.util.localization.LocalizationValidationResult;
@@ -117,7 +118,7 @@ public class IrisLanguageTest {
for (String locale : VolmitLocales.nonEnglish()) {
Path target = IrisLanguage.remote(dataFolder).cacheFile(locale);
Files.createDirectories(target.getParent());
Files.copy(Path.of("src/main/resources/languages", locale + ".json"), target, StandardCopyOption.REPLACE_EXISTING);
Files.copy(ProjectPaths.moduleFile("src/main/resources/languages").resolve(locale + ".json"), target, StandardCopyOption.REPLACE_EXISTING);
}
}
@@ -127,7 +128,7 @@ public class IrisLanguageTest {
}
private LocaleOverlay loadSourceOverlay(String locale) throws Exception {
Path source = Path.of("src/main/resources/languages", locale + ".json");
Path source = ProjectPaths.moduleFile("src/main/resources/languages").resolve(locale + ".json");
return IrisLanguage.parseDownloadedOverlay(source.toString(), locale, Files.readString(source));
}
@@ -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());
@@ -61,11 +61,12 @@ public class PackDownloadExecutionTest {
public void cancellationInterruptsRunningWorkOutsidePublication() throws Exception {
LifecycleOperationCoordinator.Lease lease = mock(LifecycleOperationCoordinator.Lease.class);
CountDownLatch started = new CountDownLatch(1);
CountDownLatch neverReleased = new CountDownLatch(1);
AtomicBoolean interrupted = new AtomicBoolean();
PackDownloadExecution execution = new PackDownloadExecution(lease, cancellation -> {
started.countDown();
try {
Thread.sleep(TimeUnit.SECONDS.toMillis(30L));
neverReleased.await(30L, TimeUnit.SECONDS);
} catch (InterruptedException exception) {
interrupted.set(true);
Thread.currentThread().interrupt();
@@ -27,9 +27,11 @@ import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.spi.PlatformStructureHooks;
import art.arcane.iris.testsupport.PlatformLeakGuard;
import org.junit.Assume;
import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
@@ -67,6 +69,9 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class PackDownloaderTest {
@ClassRule
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
@Rule
public TemporaryFolder temp = new TemporaryFolder();
@@ -154,47 +154,4 @@ public class PackExportClosureTest {
* Source guard: both packagers must export the ambient-spawning graph. Spawner and marker
* folders were silently omitted from exports, leaving dangling entitySpawners references.
*/
@Test
public void bothPackagersExportSpawnersMarkersAndTheFullEntityGraph() throws Exception {
String bukkit = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/project/IrisPackageCompiler.java")).replace("\r\n", "\n");
assertTrue("Bukkit packager must write spawners/", bukkit.contains("\"spawners/\""));
assertTrue("Bukkit packager must write markers/", bukkit.contains("\"markers/\""));
assertTrue("Bukkit packager must export region objects alongside biome objects",
bukkit.contains("regions.forEach((r) -> allPlacements.addAll(r.getObjects()))"));
assertTrue("Bukkit packager must export static objects",
bukkit.contains("exportedDimension.getStaticObjects()"));
assertTrue("Bukkit obfuscation must rewrite static object references",
bukkit.contains("placement.setObject(renameObjects.get(placement.getObject()))"));
assertTrue("Bukkit packager must export entity loot tables", bukkit.contains("getLoot().getTables()"));
assertTrue("Bukkit packager must include dimension-stack resources",
bukkit.contains("PackExportClosure.collectDimensionKeys(dimension)"));
assertTrue("Bukkit packager must export expression-backed stack blend styles",
bukkit.contains("dm.getExpressionLoader().getPossibleKeys()")
&& bukkit.contains("\"expressions/\""));
int bukkitValidation = bukkit.indexOf("PackValidator.validateForPackaging(project.getPath())");
assertTrue("Bukkit packager must validate before opening or mutating package state",
bukkitValidation >= 0
&& bukkitValidation < bukkit.indexOf("IrisData.openRuntime(project.getPath())")
&& bukkitValidation < bukkit.indexOf("IO.delete(folder)"));
String modded = Files.readString(Path.of(
"../adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedStudioCommands.java")).replace("\r\n", "\n");
assertTrue("modded packager must write spawners/", modded.contains("\"spawners\""));
assertTrue("modded packager must write markers/", modded.contains("\"markers\""));
assertTrue("modded packager must include initial spawns", modded.contains("getInitialSpawns"));
assertTrue("modded packager must export region objects", modded.contains("region.getObjects()"));
assertTrue("modded packager must export static objects",
modded.contains("PackExportClosure.collectStaticObjectKeys(exportedDimension.getStaticObjects())"));
assertTrue("modded packager must include dimension-stack resources",
modded.contains("PackExportClosure.collectDimensionKeys(dimension)"));
assertTrue("modded packager must export expression-backed stack blend styles",
modded.contains("dm.getExpressionLoader().getPossibleKeys()")
&& modded.contains("\"expressions\""));
assertTrue("modded packager must retain all snippet types and nested references",
modded.contains("PackExportClosure.copySnippets(packFolder, folder)"));
int moddedValidation = modded.indexOf("PackValidator.validateForPackaging(packFolder)");
assertTrue("modded packager must validate before mutating package state",
moddedValidation >= 0 && moddedValidation < modded.indexOf("IO.delete(folder)"));
}
}
@@ -3,8 +3,10 @@ package art.arcane.iris.core.pack;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.iris.testsupport.PlatformLeakGuard;
import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
@@ -25,6 +27,9 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class PackImageMapValidatorTest {
@ClassRule
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@@ -7,6 +7,8 @@ import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.iris.spi.PlatformStructureHooks;
import art.arcane.iris.testsupport.PlatformLeakGuard;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.Assume;
@@ -30,6 +32,9 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class PackValidationCacheTest {
@ClassRule
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@@ -2,178 +2,172 @@ package art.arcane.iris.core.pack;
import org.junit.Rule;
import org.junit.Test;
import org.junit.experimental.runners.Enclosed;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Collection;
import java.util.List;
import java.util.function.Function;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
@RunWith(Enclosed.class)
public class PackValidatorCustomBiomeSpawnTest {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
private static final Function<String, PackSpawnValidator.SpawnCategoryResolution> MONSTER =
key -> PackSpawnValidator.SpawnCategoryResolution.known("monster");
private static final Function<String, PackSpawnValidator.SpawnCategoryResolution> MISC =
key -> PackSpawnValidator.SpawnCategoryResolution.known("misc");
private static final Function<String, PackSpawnValidator.SpawnCategoryResolution> AXOLOTLS =
key -> PackSpawnValidator.SpawnCategoryResolution.known("axolotls");
private static final Function<String, PackSpawnValidator.SpawnCategoryResolution> UNKNOWN =
key -> PackSpawnValidator.SpawnCategoryResolution.unknown();
@Test
public void acceptsExplicitMatchingSpawnGroup() throws Exception {
File biomes = createBiomes("{\"customDerivitives\":[{\"id\":\"swamp\",\"spawns\":[{\"type\":\"minecraft:slime\",\"group\":\"MONSTER\"}]}]}");
List<String> errors = PackSpawnValidator.validateCustomBiomeSpawns(
biomes, key -> PackSpawnValidator.SpawnCategoryResolution.known("monster"));
assertTrue(errors.isEmpty());
}
@Test
public void acceptsBiomeWithoutCustomSpawns() throws Exception {
File biomes = createBiomes("{\"name\":\"Plains\"}");
List<String> errors = PackSpawnValidator.validateCustomBiomeSpawns(
biomes, key -> PackSpawnValidator.SpawnCategoryResolution.known("monster"));
assertTrue(errors.isEmpty());
}
@Test
public void rejectsMissingSpawnGroup() throws Exception {
File biomes = createBiomes("{\"customDerivitives\":[{\"id\":\"swamp\",\"spawns\":[{\"type\":\"minecraft:slime\"}]}]}");
List<String> errors = PackSpawnValidator.validateCustomBiomeSpawns(
biomes, key -> PackSpawnValidator.SpawnCategoryResolution.known("monster"));
assertEquals(1, errors.size());
assertTrue(errors.get(0).contains("must declare group 'MONSTER'"));
}
@Test
public void acceptsImplicitMiscSpawnGroup() throws Exception {
File biomes = createBiomes("{\"customDerivitives\":[{\"id\":\"effects\",\"spawns\":[{\"type\":\"minecraft:armor_stand\"}]}]}");
List<String> errors = PackSpawnValidator.validateCustomBiomeSpawns(
biomes, key -> PackSpawnValidator.SpawnCategoryResolution.known("misc"));
assertTrue(errors.isEmpty());
}
@Test
public void rejectsSpawnGroupThatDisagreesWithRegistry() throws Exception {
File biomes = createBiomes("{\"customDerivitives\":[{\"id\":\"swamp\",\"spawns\":[{\"type\":\"minecraft:slime\",\"group\":\"MISC\"}]}]}");
List<String> errors = PackSpawnValidator.validateCustomBiomeSpawns(
biomes, key -> PackSpawnValidator.SpawnCategoryResolution.known("monster"));
assertEquals(1, errors.size());
assertTrue(errors.get(0).contains("live entity registry requires 'MONSTER'"));
}
@Test
public void acceptsAxolotlSpawnCategory() throws Exception {
File biomes = createBiomes("{\"customDerivitives\":[{\"id\":\"cave\",\"spawns\":[{\"type\":\"minecraft:axolotl\",\"group\":\"AXOLOTLS\"}]}]}");
List<String> errors = PackSpawnValidator.validateCustomBiomeSpawns(
biomes, key -> PackSpawnValidator.SpawnCategoryResolution.known("axolotls"));
assertTrue(errors.isEmpty());
}
@Test
public void rejectsUnknownSpawnEntity() throws Exception {
File biomes = createBiomes("{\"customDerivitives\":[{\"id\":\"swamp\",\"spawns\":[{\"type\":\"missing:entity\",\"group\":\"MISC\"}]}]}");
List<String> errors = PackSpawnValidator.validateCustomBiomeSpawns(
biomes, key -> PackSpawnValidator.SpawnCategoryResolution.unknown());
assertEquals(1, errors.size());
assertTrue(errors.get(0).contains("unknown entity type 'missing:entity'"));
}
@Test
public void rejectsCaseNormalizedGroupThatRuntimeWouldNotParse() throws Exception {
File biomes = createBiomes("{\"customDerivitives\":[{\"id\":\"swamp\",\"spawns\":[{\"type\":\"minecraft:slime\",\"group\":\"monster\"}]}]}");
List<String> errors = PackSpawnValidator.validateCustomBiomeSpawns(
biomes, key -> PackSpawnValidator.SpawnCategoryResolution.known("monster"));
assertEquals(1, errors.size());
assertTrue(errors.get(0).contains("unknown group 'monster'"));
}
@Test
public void rejectsWrongCustomDerivativeContainerType() throws Exception {
File biomes = createBiomes("{\"customDerivitives\":{}}");
List<String> errors = PackSpawnValidator.validateCustomBiomeSpawns(biomes, null);
assertEquals(1, errors.size());
assertTrue(errors.get(0).contains("customDerivitives must be an array"));
}
@Test
public void rejectsWrongSpawnContainerType() throws Exception {
File biomes = createBiomes("{\"customDerivitives\":[{\"id\":\"swamp\",\"spawns\":{}}]}");
List<String> errors = PackSpawnValidator.validateCustomBiomeSpawns(biomes, null);
assertEquals(1, errors.size());
assertTrue(errors.get(0).contains("spawns must be an array"));
}
@Test
public void acceptsNullCustomDerivativeContainerAsAbsent() throws Exception {
File biomes = createBiomes("{\"customDerivitives\":null}");
List<String> errors = PackSpawnValidator.validateCustomBiomeSpawns(biomes, null);
assertTrue(errors.isEmpty());
}
@Test
public void acceptsNullSpawnContainerAsAbsent() throws Exception {
File biomes = createBiomes("{\"customDerivitives\":[{\"id\":\"swamp\",\"spawns\":null}]}");
List<String> errors = PackSpawnValidator.validateCustomBiomeSpawns(biomes, null);
assertTrue(errors.isEmpty());
}
@Test
public void acceptsNamespacedCustomBiomeTags() throws Exception {
File biomes = createBiomes("{\"customDerivitives\":[{\"id\":\"swamp\",\"tags\":[\"minecraft:allows_surface_slime_spawns\"]}]}");
List<String> errors = PackSpawnValidator.validateCustomBiomeSpawns(biomes, null);
assertTrue(errors.isEmpty());
}
@Test
public void rejectsUnsafeCustomBiomeTags() throws Exception {
File biomes = createBiomes("{\"customDerivitives\":[{\"id\":\"swamp\",\"tags\":[\"minecraft:../outside\"]}]}");
List<String> errors = PackSpawnValidator.validateCustomBiomeSpawns(biomes, null);
assertEquals(1, errors.size());
assertTrue(errors.get(0).contains("invalid tag"));
}
@Test
public void convertsSpawnCategoryResolverFailureIntoBlockingError() throws Exception {
File biomes = createBiomes("{\"customDerivitives\":[{\"id\":\"swamp\",\"spawns\":[{\"type\":\"minecraft:slime\",\"group\":\"MONSTER\"}]}]}");
List<String> errors = PackSpawnValidator.validateCustomBiomeSpawns(biomes, key -> {
throw new IllegalStateException("registry unavailable");
});
assertEquals(1, errors.size());
assertTrue(errors.get(0).contains("spawn category lookup failed"));
assertTrue(errors.get(0).contains("registry unavailable"));
}
private File createBiomes(String json) throws Exception {
private static File createBiomes(TemporaryFolder temporaryFolder, String json) throws Exception {
File biomes = temporaryFolder.newFolder("biomes");
File biome = new File(biomes, "test.json");
Files.writeString(biome.toPath(), json, StandardCharsets.UTF_8);
return biomes;
}
@RunWith(Parameterized.class)
public static class Accepted {
@Parameters(name = "{0}")
public static Collection<Object[]> biomes() {
return List.of(
new Object[]{"explicitMatchingSpawnGroup",
"{\"customDerivitives\":[{\"id\":\"swamp\",\"spawns\":[{\"type\":\"minecraft:slime\",\"group\":\"MONSTER\"}]}]}",
MONSTER},
new Object[]{"biomeWithoutCustomSpawns",
"{\"name\":\"Plains\"}",
MONSTER},
new Object[]{"implicitMiscSpawnGroup",
"{\"customDerivitives\":[{\"id\":\"effects\",\"spawns\":[{\"type\":\"minecraft:armor_stand\"}]}]}",
MISC},
new Object[]{"axolotlSpawnCategory",
"{\"customDerivitives\":[{\"id\":\"cave\",\"spawns\":[{\"type\":\"minecraft:axolotl\",\"group\":\"AXOLOTLS\"}]}]}",
AXOLOTLS},
new Object[]{"nullCustomDerivativeContainerAsAbsent",
"{\"customDerivitives\":null}",
null},
new Object[]{"nullSpawnContainerAsAbsent",
"{\"customDerivitives\":[{\"id\":\"swamp\",\"spawns\":null}]}",
null},
new Object[]{"namespacedCustomBiomeTags",
"{\"customDerivitives\":[{\"id\":\"swamp\",\"tags\":[\"minecraft:allows_surface_slime_spawns\"]}]}",
null}
);
}
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
private final String scenario;
private final String json;
private final Function<String, PackSpawnValidator.SpawnCategoryResolution> categoryResolver;
public Accepted(
String scenario,
String json,
Function<String, PackSpawnValidator.SpawnCategoryResolution> categoryResolver
) {
this.scenario = scenario;
this.json = json;
this.categoryResolver = categoryResolver;
}
@Test
public void reportsNoSpawnErrors() throws Exception {
File biomes = createBiomes(temporaryFolder, json);
List<String> errors = PackSpawnValidator.validateCustomBiomeSpawns(biomes, categoryResolver);
assertTrue(scenario + " " + errors, errors.isEmpty());
}
}
@RunWith(Parameterized.class)
public static class Rejected {
@Parameters(name = "{0}")
public static Collection<Object[]> biomes() {
return List.of(
new Object[]{"missingSpawnGroup",
"{\"customDerivitives\":[{\"id\":\"swamp\",\"spawns\":[{\"type\":\"minecraft:slime\"}]}]}",
MONSTER, "must declare group 'MONSTER'"},
new Object[]{"spawnGroupThatDisagreesWithRegistry",
"{\"customDerivitives\":[{\"id\":\"swamp\",\"spawns\":[{\"type\":\"minecraft:slime\",\"group\":\"MISC\"}]}]}",
MONSTER, "live entity registry requires 'MONSTER'"},
new Object[]{"unknownSpawnEntity",
"{\"customDerivitives\":[{\"id\":\"swamp\",\"spawns\":[{\"type\":\"missing:entity\",\"group\":\"MISC\"}]}]}",
UNKNOWN, "unknown entity type 'missing:entity'"},
new Object[]{"caseNormalizedGroupThatRuntimeWouldNotParse",
"{\"customDerivitives\":[{\"id\":\"swamp\",\"spawns\":[{\"type\":\"minecraft:slime\",\"group\":\"monster\"}]}]}",
MONSTER, "unknown group 'monster'"},
new Object[]{"wrongCustomDerivativeContainerType",
"{\"customDerivitives\":{}}",
null, "customDerivitives must be an array"},
new Object[]{"wrongSpawnContainerType",
"{\"customDerivitives\":[{\"id\":\"swamp\",\"spawns\":{}}]}",
null, "spawns must be an array"},
new Object[]{"unsafeCustomBiomeTags",
"{\"customDerivitives\":[{\"id\":\"swamp\",\"tags\":[\"minecraft:../outside\"]}]}",
null, "invalid tag"}
);
}
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
private final String scenario;
private final String json;
private final Function<String, PackSpawnValidator.SpawnCategoryResolution> categoryResolver;
private final String expectedError;
public Rejected(
String scenario,
String json,
Function<String, PackSpawnValidator.SpawnCategoryResolution> categoryResolver,
String expectedError
) {
this.scenario = scenario;
this.json = json;
this.categoryResolver = categoryResolver;
this.expectedError = expectedError;
}
@Test
public void reportsExactlyOneSpawnError() throws Exception {
File biomes = createBiomes(temporaryFolder, json);
List<String> errors = PackSpawnValidator.validateCustomBiomeSpawns(biomes, categoryResolver);
assertEquals(scenario + " " + errors, 1, errors.size());
assertTrue(errors.get(0), errors.get(0).contains(expectedError));
}
}
public static class ResolverFailure {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void convertsSpawnCategoryResolverFailureIntoBlockingError() throws Exception {
File biomes = createBiomes(temporaryFolder,
"{\"customDerivitives\":[{\"id\":\"swamp\",\"spawns\":[{\"type\":\"minecraft:slime\",\"group\":\"MONSTER\"}]}]}");
List<String> errors = PackSpawnValidator.validateCustomBiomeSpawns(biomes, key -> {
throw new IllegalStateException("registry unavailable");
});
assertEquals(1, errors.size());
assertTrue(errors.get(0).contains("spawn category lookup failed"));
assertTrue(errors.get(0).contains("registry unavailable"));
}
}
}
@@ -4,6 +4,8 @@ import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformStructureHooks;
import art.arcane.iris.spi.PlatformStructureHooks.JigsawSourceMetadata;
import art.arcane.iris.testsupport.PlatformLeakGuard;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
@@ -27,6 +29,9 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class PackValidatorStructureGraphTest {
@ClassRule
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@@ -0,0 +1,47 @@
package art.arcane.iris.core.pregenerator;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class MantleHeapPressureUrgencyTest {
@Test
public void headroomBelowTheLowWaterMarkAsksForNothing() {
assertEquals(0D, MantleHeapPressure.reclaimUrgency(0D), 0D);
assertEquals(0D, MantleHeapPressure.reclaimUrgency(0.5D), 0D);
assertEquals(0D, MantleHeapPressure.reclaimUrgency(0.82D), 0D);
}
@Test
public void pressureAtOrAboveTheHighWaterMarkIsFullyUrgent() {
assertEquals(1D, MantleHeapPressure.reclaimUrgency(0.92D), 0D);
assertEquals(1D, MantleHeapPressure.reclaimUrgency(0.99D), 0D);
assertEquals(1D, MantleHeapPressure.reclaimUrgency(4D), 0D);
}
@Test
public void pressureBetweenTheMarksRampsLinearly() {
assertEquals(0.25D, MantleHeapPressure.reclaimUrgency(0.845D), 1.0E-9D);
assertEquals(0.5D, MantleHeapPressure.reclaimUrgency(0.87D), 1.0E-9D);
assertEquals(0.75D, MantleHeapPressure.reclaimUrgency(0.895D), 1.0E-9D);
}
@Test
public void urgencyNeverDecreasesAsPressureRises() {
double previous = -1D;
for (int step = 0; step <= 100; step++) {
double urgency = MantleHeapPressure.reclaimUrgency(step / 100D);
assertTrue("urgency dropped at " + step, urgency >= previous);
previous = urgency;
}
}
@Test
public void unmeasurableHeapPressureAsksForNothing() {
assertEquals(0D, MantleHeapPressure.reclaimUrgency(Double.NaN), 0D);
assertEquals(0D, MantleHeapPressure.reclaimUrgency(Double.POSITIVE_INFINITY), 0D);
assertEquals(0D, MantleHeapPressure.reclaimUrgency(Double.NEGATIVE_INFINITY), 0D);
}
}
@@ -0,0 +1,189 @@
package art.arcane.iris.core.pregenerator;
import org.junit.Test;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
public class PregenAdmissionGateTest {
private static final long GENEROUS_BOUND_MS = 60_000L;
@Test
public void admissionConsumesOnePermitAndReturnsItOnRelease() throws Exception {
PregenAdmissionGate gate = new PregenAdmissionGate(2, GENEROUS_BOUND_MS, System::currentTimeMillis);
assertEquals(2, gate.availablePermits());
assertNotNull(gate.admit(() -> false, () -> false));
assertEquals(1, gate.availablePermits());
gate.release();
assertEquals(2, gate.availablePermits());
}
@Test
public void releaseNeverPushesThePermitCountAboveTheConfiguredCeiling() throws Exception {
PregenAdmissionGate gate = new PregenAdmissionGate(1, GENEROUS_BOUND_MS, System::currentTimeMillis);
gate.release();
gate.release();
assertEquals(1, gate.availablePermits());
assertNotNull(gate.admit(() -> false, () -> false));
assertEquals(0, gate.availablePermits());
}
@Test
public void aBlockedAdmissionWakesOnTheReleaseSignalRatherThanTheTimeout() throws Exception {
PregenAdmissionGate gate = new PregenAdmissionGate(1, GENEROUS_BOUND_MS, System::currentTimeMillis);
assertNotNull(gate.admit(() -> false, () -> false));
CountDownLatch started = new CountDownLatch(1);
ExecutorService executor = Executors.newSingleThreadExecutor();
try {
Future<PregenAdmissionGate.Wait> blocked = executor.submit(() -> {
started.countDown();
return gate.admit(() -> false, () -> false);
});
assertTrue(started.await(5, TimeUnit.SECONDS));
while (gate.availablePermits() != 0) {
Thread.onSpinWait();
}
gate.release();
assertNotNull(blocked.get(5, TimeUnit.SECONDS));
} finally {
executor.shutdownNow();
}
}
@Test
public void aBlockedAdmissionWakesOnTheAdaptiveLimitSignalWithoutWaitingOutTheBound() throws Exception {
PregenAdmissionGate gate = new PregenAdmissionGate(4, GENEROUS_BOUND_MS, System::currentTimeMillis);
AtomicBoolean overLimit = new AtomicBoolean(true);
CountDownLatch started = new CountDownLatch(1);
ExecutorService executor = Executors.newSingleThreadExecutor();
try {
Future<PregenAdmissionGate.Wait> blocked = executor.submit(() -> {
started.countDown();
return gate.admit(overLimit::get, () -> false);
});
assertTrue(started.await(5, TimeUnit.SECONDS));
Thread.yield();
overLimit.set(false);
gate.wake();
assertNotNull(blocked.get(5, TimeUnit.SECONDS));
assertEquals(3, gate.availablePermits());
} finally {
executor.shutdownNow();
}
}
@Test
public void aCancelledAdmissionReturnsNothingAndConsumesNoPermit() throws Exception {
PregenAdmissionGate gate = new PregenAdmissionGate(1, GENEROUS_BOUND_MS, System::currentTimeMillis);
assertNull(gate.admit(() -> true, () -> true));
assertEquals(1, gate.availablePermits());
}
@Test
public void aCancellationRaisedWhileBlockedEndsTheWait() throws Exception {
PregenAdmissionGate gate = new PregenAdmissionGate(1, GENEROUS_BOUND_MS, System::currentTimeMillis);
assertNotNull(gate.admit(() -> false, () -> false));
AtomicBoolean cancelled = new AtomicBoolean();
CountDownLatch started = new CountDownLatch(1);
ExecutorService executor = Executors.newSingleThreadExecutor();
try {
Future<PregenAdmissionGate.Wait> blocked = executor.submit(() -> {
started.countDown();
return gate.admit(() -> false, cancelled::get);
});
assertTrue(started.await(5, TimeUnit.SECONDS));
Thread.yield();
cancelled.set(true);
gate.wake();
assertNull(blocked.get(5, TimeUnit.SECONDS));
assertEquals(0, gate.availablePermits());
} finally {
executor.shutdownNow();
}
}
@Test
public void theAdaptiveAndPermitWaitsAreAttributedSeparately() throws Exception {
AtomicLong clock = new AtomicLong();
AtomicBoolean overLimit = new AtomicBoolean(true);
PregenAdmissionGate gate = new PregenAdmissionGate(1, 5L, () -> clock.addAndGet(10L));
assertNotNull(gate.admit(() -> false, () -> false));
AtomicInteger passes = new AtomicInteger();
ExecutorService executor = Executors.newSingleThreadExecutor();
try {
Future<PregenAdmissionGate.Wait> blocked = executor.submit(() -> gate.admit(() -> {
if (passes.incrementAndGet() > 2) {
overLimit.set(false);
}
return overLimit.get();
}, () -> false));
while (passes.get() <= 3) {
Thread.onSpinWait();
}
gate.release();
PregenAdmissionGate.Wait wait = blocked.get(5, TimeUnit.SECONDS);
assertNotNull(wait);
assertTrue("adaptive wait was not attributed: " + wait, wait.adaptiveMs() > 0L);
assertTrue("permit wait was not attributed: " + wait, wait.permitMs() > 0L);
} finally {
executor.shutdownNow();
}
}
@Test
public void drainReturnsOnlyAfterEveryPermitIsBack() throws Exception {
PregenAdmissionGate gate = new PregenAdmissionGate(2, GENEROUS_BOUND_MS, System::currentTimeMillis);
assertNotNull(gate.admit(() -> false, () -> false));
assertNotNull(gate.admit(() -> false, () -> false));
AtomicInteger warnings = new AtomicInteger();
ExecutorService executor = Executors.newSingleThreadExecutor();
try {
Future<Boolean> drain = executor.submit(() -> gate.awaitDrain(5L, TimeUnit.MILLISECONDS, warnings::incrementAndGet));
while (warnings.get() < 2) {
Thread.onSpinWait();
}
gate.release();
gate.release();
assertEquals(Boolean.FALSE, drain.get(5, TimeUnit.SECONDS));
assertEquals(2, gate.availablePermits());
} finally {
executor.shutdownNow();
}
}
@Test(expected = IllegalArgumentException.class)
public void aGateWithoutPermitsIsRejected() {
new PregenAdmissionGate(0, GENEROUS_BOUND_MS, System::currentTimeMillis);
}
@Test(expected = IllegalArgumentException.class)
public void aGateWithoutAWaitBoundIsRejected() {
new PregenAdmissionGate(1, 0L, System::currentTimeMillis);
}
}
@@ -0,0 +1,40 @@
package art.arcane.iris.core.pregenerator;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class PregenDiagnosticsDescriptionTest {
@Test
public void anUnnamedProbeStillReadsAsAProbe() {
assertEquals("unnamed", PregenDiagnostics.describeProbe(null));
assertEquals("unnamed", PregenDiagnostics.describeProbe(""));
assertEquals("unnamed", PregenDiagnostics.describeProbe(" "));
}
@Test
public void aNamedProbeIsReportedVerbatim() {
assertEquals("mantle handle for backpressure",
PregenDiagnostics.describeProbe("mantle handle for backpressure"));
assertEquals("%s throughput", PregenDiagnostics.describeProbe("%s throughput"));
}
@Test
public void anAbsentThrowableIsCalledOutInsteadOfPrintingNull() {
assertEquals("no throwable reported", PregenDiagnostics.describeFailure(null));
}
@Test
public void aThrowableWithoutAMessageIsIdentifiedByItsType() {
assertEquals("java.lang.IllegalStateException",
PregenDiagnostics.describeFailure(new IllegalStateException()));
}
@Test
public void aThrowableWithAMessageCarriesBothTypeAndMessage() {
assertEquals("java.lang.IllegalStateException: mantle is gone",
PregenDiagnostics.describeFailure(new IllegalStateException("mantle is gone")));
assertEquals("java.lang.RuntimeException: ",
PregenDiagnostics.describeFailure(new RuntimeException("")));
}
}
@@ -0,0 +1,84 @@
package art.arcane.iris.core.pregenerator;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.LogLevel;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentMatchers;
import java.util.ArrayList;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
public class PregenDiagnosticsTest {
private final List<String> emitted = new ArrayList<>();
private final List<LogLevel> levels = new ArrayList<>();
private IrisPlatform previousPlatform;
@Before
public void captureLog() {
previousPlatform = IrisPlatforms.isBound() ? IrisPlatforms.get() : null;
IrisPlatforms.unbind();
emitted.clear();
levels.clear();
IrisPlatform platform = mock(IrisPlatform.class);
doAnswer(invocation -> {
levels.add(invocation.getArgument(0, LogLevel.class));
emitted.add(invocation.getArgument(1, String.class));
return null;
}).when(platform).log(ArgumentMatchers.any(LogLevel.class), ArgumentMatchers.anyString());
IrisPlatforms.bind(platform);
}
@After
public void restorePlatform() {
IrisPlatforms.unbind();
if (previousPlatform != null) {
IrisPlatforms.bind(previousPlatform);
}
}
@Test
public void aFailedProbeReportsItsNameClassAndMessageAtDebug() {
PregenDiagnostics.probeFailed("moonrise worker pool size", new IllegalStateException("no such field"));
assertEquals(1, emitted.size());
assertEquals(LogLevel.DEBUG, levels.get(0));
assertTrue(emitted.get(0), emitted.get(0).contains("moonrise worker pool size"));
assertTrue(emitted.get(0), emitted.get(0).contains(IllegalStateException.class.getName()));
assertTrue(emitted.get(0), emitted.get(0).contains("no such field"));
}
@Test
public void aThrowableWithNoMessageStillReportsItsClass() {
PregenDiagnostics.probeFailed("engine access", new NoClassDefFoundError());
assertEquals(1, emitted.size());
assertTrue(emitted.get(0), emitted.get(0).contains(NoClassDefFoundError.class.getName()));
assertTrue(emitted.get(0), emitted.get(0).contains("engine access"));
}
@Test
public void aMissingProbeNameAndThrowableStillProduceAReadableLine() {
PregenDiagnostics.probeFailed(null, null);
assertEquals(1, emitted.size());
assertTrue(emitted.get(0), emitted.get(0).contains("unnamed"));
assertTrue(emitted.get(0), emitted.get(0).contains("no throwable reported"));
}
@Test
public void percentSignsInAProbeNameAreNotTreatedAsFormatSpecifiers() {
PregenDiagnostics.probeFailed("heap at 95% headroom", new IllegalArgumentException("bad %s value"));
assertEquals(1, emitted.size());
assertTrue(emitted.get(0), emitted.get(0).contains("heap at 95% headroom"));
assertTrue(emitted.get(0), emitted.get(0).contains("bad %s value"));
}
}
@@ -0,0 +1,104 @@
package art.arcane.iris.core.pregenerator;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class PregenListenerDefaultsTest {
@Test
public void anUnqualifiedChunkIsReportedAsFreshlyGenerated() {
RecordingListener listener = new RecordingListener();
listener.onChunkGenerated(4, -9);
assertEquals(List.of("4/-9/false"), listener.generated);
}
@Test
public void anExplicitCacheFlagIsPassedThroughUnchanged() {
RecordingListener listener = new RecordingListener();
listener.onChunkGenerated(1, 2, true);
listener.onChunkGenerated(3, 4, false);
assertEquals(List.of("1/2/true", "3/4/false"), listener.generated);
}
@Test
public void aFailedChunkIsIgnoredByListenersThatDoNotCare() {
RecordingListener listener = new RecordingListener();
listener.onChunkFailed(7, 7);
assertTrue(listener.generated.isEmpty());
}
private static final class RecordingListener implements PregenListener {
private final List<String> generated = new ArrayList<>();
@Override
public void onTick(double chunksPerSecond, double chunksPerMinute, double regionsPerMinute, double percent, long generatedChunks, long totalChunks, long chunksRemaining, long eta, long elapsed, String method, boolean cached) {
}
@Override
public void onChunkGenerating(int x, int z) {
}
@Override
public void onChunkGenerated(int x, int z, boolean cached) {
generated.add(x + "/" + z + "/" + cached);
}
@Override
public void onRegionGenerated(int x, int z) {
}
@Override
public void onRegionGenerating(int x, int z) {
}
@Override
public void onChunkCleaned(int x, int z) {
}
@Override
public void onRegionSkipped(int x, int z) {
}
@Override
public void onNetworkStarted(int x, int z) {
}
@Override
public void onNetworkFailed(int x, int z) {
}
@Override
public void onNetworkReclaim(int revert) {
}
@Override
public void onNetworkGeneratedChunk(int x, int z) {
}
@Override
public void onNetworkDownloaded(int x, int z) {
}
@Override
public void onClose() {
}
@Override
public void onSaving() {
}
@Override
public void onChunkExistsInRegionGen(int x, int z) {
}
}
}
@@ -0,0 +1,152 @@
package art.arcane.iris.core.pregenerator;
import art.arcane.iris.testsupport.Await;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import org.junit.Test;
import java.time.Duration;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
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 PregenMantleBackpressureTest {
@Test
public void aDisabledPlateCapNeverAsksForAMantle() {
AtomicInteger supplierCalls = new AtomicInteger();
Supplier<Mantle> supplier = () -> {
supplierCalls.incrementAndGet();
return null;
};
backpressure(supplier, 0, 1_000L, () -> {
}).enforceMantleBudget();
assertEquals(0, supplierCalls.get());
}
@Test
public void aFailingMantleProbeDoesNotPropagate() {
AtomicInteger timeouts = new AtomicInteger();
backpressure(() -> {
throw new IllegalStateException("no mantle here");
}, 4, 1_000L, timeouts::incrementAndGet).enforceMantleBudget();
assertEquals(0, timeouts.get());
}
@Test
public void residencyAtTwiceThePlateCapIsNotTrimmed() {
Mantle mantle = mock(Mantle.class);
when(mantle.getLoadedRegionCount()).thenReturn(8);
backpressure(() -> mantle, 4, 1_000L, () -> {
}).enforceMantleBudget();
verify(mantle, never()).trim(0L, 0);
verify(mantle, never()).unloadTectonicPlate(0);
}
@Test
public void residencyOverTheHardCapIsTrimmedUntilItFits() {
Mantle mantle = mock(Mantle.class);
when(mantle.getLoadedRegionCount()).thenReturn(20, 20, 3);
backpressure(() -> mantle, 4, 1_000L, () -> {
}).enforceMantleBudget();
verify(mantle, times(1)).trim(0L, 0);
verify(mantle, times(1)).unloadTectonicPlate(0);
}
@Test
public void aTrimFailureEndsTheWaitInsteadOfSpinning() {
Mantle mantle = mock(Mantle.class);
when(mantle.getLoadedRegionCount()).thenReturn(20);
doThrow(new IllegalStateException("mantle is gone")).when(mantle).trim(0L, 0);
AtomicInteger timeouts = new AtomicInteger();
backpressure(() -> mantle, 4, 1_000L, timeouts::incrementAndGet).enforceMantleBudget();
verify(mantle, never()).unloadTectonicPlate(0);
assertEquals(0, timeouts.get());
}
@Test
public void anExhaustedBudgetReportsTheTimeoutOnceAndProceeds() {
Mantle mantle = mock(Mantle.class);
when(mantle.getLoadedRegionCount()).thenReturn(20);
AtomicInteger timeouts = new AtomicInteger();
backpressure(() -> mantle, 4, 0L, timeouts::incrementAndGet).enforceMantleBudget();
assertEquals(1, timeouts.get());
}
@Test
public void cancellationStopsTheWaitWithoutReportingATimeout() {
Mantle mantle = mock(Mantle.class);
when(mantle.getLoadedRegionCount()).thenReturn(20);
AtomicInteger timeouts = new AtomicInteger();
PregenMantleBackpressure backpressure = new PregenMantleBackpressure(
() -> mantle, 4, 5, 0L, timeouts::incrementAndGet, () -> "cancelled", () -> true);
backpressure.enforceMantleBudget();
verify(mantle, never()).trim(0L, 0);
verify(mantle, never()).unloadTectonicPlate(0);
assertEquals(0, timeouts.get());
}
@Test
public void aFailingCancellationProbeIsTreatedAsRunning() {
Mantle mantle = mock(Mantle.class);
when(mantle.getLoadedRegionCount()).thenReturn(20);
AtomicInteger timeouts = new AtomicInteger();
PregenMantleBackpressure backpressure = new PregenMantleBackpressure(
() -> mantle, 4, 5, 0L, timeouts::incrementAndGet, () -> "probing", () -> {
throw new IllegalStateException("cancellation probe exploded");
});
backpressure.enforceMantleBudget();
assertEquals(1, timeouts.get());
}
@Test
public void signalledProgressReleasesTheWaitAheadOfThePollInterval() throws Exception {
Mantle mantle = mock(Mantle.class);
when(mantle.getLoadedRegionCount()).thenReturn(20, 20, 20, 3);
PregenMantleBackpressure backpressure = new PregenMantleBackpressure(
() -> mantle, 4, 600_000, 600_000L, () -> {
}, () -> "waiting");
Thread waiter = new Thread(backpressure::enforceMantleBudget, "pregen-backpressure-waiter");
waiter.start();
Await.until("the parked backpressure wait to be released", Duration.ofSeconds(10L), () -> {
backpressure.signalProgress();
return !waiter.isAlive();
});
waiter.join(1_000L);
assertFalse(waiter.isAlive());
}
private static PregenMantleBackpressure backpressure(
Supplier<Mantle> mantleSupplier,
int maxResidentTectonicPlates,
long timeoutMs,
Runnable onBudgetTimeout
) {
return new PregenMantleBackpressure(
mantleSupplier, maxResidentTectonicPlates, 5, timeoutMs, onBudgetTimeout, () -> "test");
}
}
@@ -107,4 +107,41 @@ public class PregenPerformanceProfileTest {
assertEquals(before + 1, MultiBurst.burst.parallelism());
assertFalse(MultiBurst.burst.raiseParallelism(before));
}
@Test
public void restoringBurstParallelismReturnsThePoolToItsPrePregenerationSize() {
int before = MultiBurst.ioBurst.parallelism();
assertTrue(MultiBurst.ioBurst.raiseParallelism(before + 3));
assertEquals(before + 3, MultiBurst.ioBurst.parallelism());
assertTrue(MultiBurst.ioBurst.restoreParallelism());
assertEquals(before, MultiBurst.ioBurst.parallelism());
assertFalse(MultiBurst.ioBurst.restoreParallelism());
assertEquals(before, MultiBurst.ioBurst.parallelism());
}
@Test
public void aSecondRaiseKeepsTheOriginalBaselineSoOneRestoreIsEnough() {
int before = MultiBurst.ioBurst.parallelism();
assertTrue(MultiBurst.ioBurst.raiseParallelism(before + 2));
assertTrue(MultiBurst.ioBurst.raiseParallelism(before + 5));
assertTrue(MultiBurst.ioBurst.restoreParallelism());
assertEquals(before, MultiBurst.ioBurst.parallelism());
}
@Test
public void theBurstParallelismHoldIsReleasedByAMatchingRestore() {
while (PregenPerformanceProfile.parallelismHolders() > 0) {
PregenPerformanceProfile.restore();
}
assertFalse(PregenPerformanceProfile.restore());
assertEquals(0, PregenPerformanceProfile.parallelismHolders());
PregenPerformanceProfile.apply();
int holders = PregenPerformanceProfile.parallelismHolders();
PregenPerformanceProfile.restore();
assertEquals(Math.max(0, holders - 1), PregenPerformanceProfile.parallelismHolders());
}
}
@@ -0,0 +1,136 @@
package art.arcane.iris.core.pregenerator;
import org.junit.Test;
import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class PregenSilentFailureContractTest {
@Test
public void noPregenSourceSwallowsAThrowableWithoutSayingAnything() throws IOException {
List<String> offenders = new ArrayList<>();
Path pregenRoot = sourceRoot().resolve("art/arcane/iris/core/pregenerator");
try (Stream<Path> sources = Files.walk(pregenRoot)) {
for (Path source : sources.filter(path -> path.toString().endsWith(".java")).toList()) {
String text = Files.readString(source);
if (text.contains("catch (Throwable ignored)") || text.contains("catch (Exception ignored)")) {
offenders.add(source.getFileName().toString());
}
}
}
assertEquals("silent catches on the pregen path: " + offenders, List.of(), offenders);
}
@Test
public void mantleCleanupFailureIsReportedWithItsStackTrace() throws IOException {
String cleanup = method(read("art/arcane/iris/core/pregenerator/methods/AsyncPregenMethod.java"),
"private void cleanupMantleChunksCoveredBy(");
assertTrue(cleanup, cleanup.contains("IrisLogging.reportError("));
assertTrue(cleanup, cleanup.contains("catch (Throwable e)"));
}
@Test
public void aStrandedPluginChunkTicketIsReportedWithItsStackTrace() throws IOException {
String unload = method(read("art/arcane/iris/core/pregenerator/methods/AsyncPregenMethod.java"),
"private void unloadChunkSafely(");
assertTrue(unload, unload.contains("removePluginChunkTicket"));
assertEquals(unload, 2, countOccurrences(unload, "IrisLogging.reportError("));
}
@Test
public void capabilityProbesLogThroughTheNamedDebugHelperRatherThanReportingErrors() throws IOException {
String async = read("art/arcane/iris/core/pregenerator/methods/AsyncPregenMethod.java");
assertTrue(method(async, "private int resolveWorkerPoolThreads()").contains("PregenDiagnostics.probeFailed("));
assertTrue(method(async, "private int evictionWindow()").contains("PregenDiagnostics.probeFailed("));
assertTrue(method(async, "private Engine resolveMetricsEngine()").contains("PregenDiagnostics.probeFailed("));
}
@Test
public void theFoliaSubmitFallbackNamesTheFailureItIsFallingBackFrom() throws IOException {
String executor = method(read("art/arcane/iris/core/pregenerator/methods/AsyncPregenMethod.java"),
"private class FoliaRegionExecutor");
assertTrue(executor, executor.contains("PregenDiagnostics.probeFailed("));
assertTrue(executor, executor.contains("falling back to a region task"));
}
@Test
public void backpressureProbesAnnounceWhyTheyFellBack() throws IOException {
String backpressure = read("art/arcane/iris/core/pregenerator/PregenMantleBackpressure.java");
assertTrue(method(backpressure, "private boolean isCancelled()").contains("PregenDiagnostics.probeFailed("));
assertTrue(method(backpressure, "private Mantle resolveMantle()").contains("PregenDiagnostics.probeFailed("));
}
@Test
public void medievalEngineResolutionAnnouncesWhyItFellBack() throws IOException {
String medieval = read("art/arcane/iris/core/pregenerator/methods/MedievalPregenMethod.java");
assertTrue(method(medieval, "private Engine resolveEngine()").contains("PregenDiagnostics.probeFailed("));
}
private static int countOccurrences(String text, String needle) {
int count = 0;
int index = text.indexOf(needle);
while (index >= 0) {
count++;
index = text.indexOf(needle, index + needle.length());
}
return count;
}
private static Path sourceRoot() throws IOException {
Path current;
try {
current = Path.of(PregenSilentFailureContractTest.class.getProtectionDomain()
.getCodeSource()
.getLocation()
.toURI());
} catch (URISyntaxException e) {
throw new IOException("Unable to resolve the core source root", e);
}
while (current != null && !Files.isDirectory(current.resolve("src/main/java"))) {
current = current.getParent();
}
if (current == null) {
throw new IOException("Unable to locate the core source root");
}
return current.resolve("src/main/java");
}
private static String read(String relativePath) throws IOException {
return Files.readString(sourceRoot().resolve(relativePath));
}
private static String method(String source, String signature) {
int start = source.indexOf(signature);
assertTrue("Missing source contract signature: " + signature, start >= 0);
int openBrace = source.indexOf('{', start);
assertTrue("Missing source contract method body: " + signature, openBrace >= 0);
int depth = 0;
for (int index = openBrace; index < source.length(); index++) {
char current = source.charAt(index);
if (current == '{') {
depth++;
} else if (current == '}') {
depth--;
if (depth == 0) {
return source.substring(start, index + 1);
}
}
}
throw new IllegalArgumentException("Unclosed source contract method: " + signature);
}
}
@@ -0,0 +1,111 @@
package art.arcane.iris.core.pregenerator;
import org.junit.Test;
import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class PregenWaitPolicyContractTest {
@Test
public void noPregenSourceSleepPollsWhileWaitingForCapacity() throws IOException {
List<String> offenders = new ArrayList<>();
Path pregenRoot = sourceRoot().resolve("art/arcane/iris/core/pregenerator");
try (Stream<Path> sources = Files.walk(pregenRoot)) {
for (Path source : sources.filter(path -> path.toString().endsWith(".java")).toList()) {
String text = Files.readString(source);
if (text.contains("Thread.sleep(") || text.contains("J.sleep(")) {
offenders.add(source.getFileName().toString());
}
}
}
assertEquals("sleep polling on the pregen path: " + offenders, List.of(), offenders);
}
@Test
public void mantleBackpressureWaitsOnAProgressSignalWithABoundedTimeout() throws IOException {
String backpressure = read("art/arcane/iris/core/pregenerator/PregenMantleBackpressure.java");
assertTrue(backpressure.contains("progressed.await(waitMs, TimeUnit.MILLISECONDS)"));
assertTrue(method(backpressure, "public void enforceMantleBudget()").contains("awaitProgress()"));
assertTrue(method(backpressure, "public void awaitHeapHeadroom()").contains("awaitProgress()"));
}
@Test
public void chunkCompletionSignalsBothTheAdmissionGateAndTheBackpressureWait() throws IOException {
String finished = method(read("art/arcane/iris/core/pregenerator/methods/AsyncPregenMethod.java"),
"private void markFinished(boolean success)");
assertTrue(finished, finished.contains("admission.release()"));
assertTrue(finished, finished.contains("backpressure.signalProgress()"));
}
@Test
public void pauseResumeAndStopSignalThePregenGateInsteadOfLettingItPoll() throws IOException {
String pregenerator = read("art/arcane/iris/core/pregenerator/IrisPregenerator.java");
assertTrue(method(pregenerator, "public void pause()").contains("signalState()"));
assertTrue(method(pregenerator, "public void resume()").contains("signalState()"));
assertTrue(method(pregenerator, "public void close()").contains("signalState()"));
assertTrue(method(pregenerator, "private void visitRegion(").contains("awaitStateChange(GATE_WAIT_MS)"));
assertTrue(method(pregenerator, "private void reclaimTectonicPlates(").contains("awaitStateChange(RECLAIM_WAIT_MS)"));
}
@Test
public void aFinishedPregenReleasesTheBurstParallelismItRaised() throws IOException {
String shutdown = method(read("art/arcane/iris/core/pregenerator/IrisPregenerator.java"), "private void shutdown()");
assertTrue(shutdown, shutdown.contains("shutdownStep(\"parallelism\", PregenPerformanceProfile::restore)"));
}
private static Path sourceRoot() throws IOException {
Path current;
try {
current = Path.of(PregenWaitPolicyContractTest.class.getProtectionDomain()
.getCodeSource()
.getLocation()
.toURI());
} catch (URISyntaxException e) {
throw new IOException("Unable to resolve the core source root", e);
}
while (current != null && !Files.isDirectory(current.resolve("src/main/java"))) {
current = current.getParent();
}
if (current == null) {
throw new IOException("Unable to locate the core source root");
}
return current.resolve("src/main/java");
}
private static String read(String relativePath) throws IOException {
return Files.readString(sourceRoot().resolve(relativePath));
}
private static String method(String source, String signature) {
int start = source.indexOf(signature);
assertTrue("Missing source contract signature: " + signature, start >= 0);
int openBrace = source.indexOf('{', start);
assertTrue("Missing source contract method body: " + signature, openBrace >= 0);
int depth = 0;
for (int index = openBrace; index < source.length(); index++) {
char current = source.charAt(index);
if (current == '{') {
depth++;
} else if (current == '}') {
depth--;
if (depth == 0) {
return source.substring(start, index + 1);
}
}
}
throw new IllegalArgumentException("Unclosed source contract method: " + signature);
}
}
@@ -0,0 +1,194 @@
package art.arcane.iris.core.pregenerator.cache;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.File;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class PregenCacheConcurrencyTest {
private static final int THREADS = 8;
private File directory;
private ExecutorService pool;
@Before
public void setUp() throws Exception {
directory = Files.createTempDirectory("iris-pregen-cache-concurrency").toFile();
pool = Executors.newFixedThreadPool(THREADS);
}
@After
public void tearDown() throws Exception {
pool.shutdownNow();
pool.awaitTermination(30, TimeUnit.SECONDS);
deleteRecursively(directory);
}
@Test
public void everyConcurrentlyCachedChunkIsReadBackAsCached() throws Exception {
PregenCache cache = PregenCache.create(directory);
int chunksPerThread = 512;
runAll(thread -> () -> {
for (int index = 0; index < chunksPerThread; index++) {
int x = (thread * chunksPerThread) + index;
cache.cacheChunk(x, thread);
}
return null;
});
for (int thread = 0; thread < THREADS; thread++) {
for (int index = 0; index < chunksPerThread; index++) {
int x = (thread * chunksPerThread) + index;
assertTrue("chunk " + x + "," + thread, cache.isChunkCached(x, thread));
}
}
}
@Test
public void concurrentWritersToOneRegionNeverLoseABit() throws Exception {
PregenCache cache = PregenCache.create(directory);
runAll(thread -> () -> {
for (int index = thread; index < 1024; index += THREADS) {
cache.cacheChunk(index & 31, index >> 5);
}
return null;
});
for (int index = 0; index < 1024; index++) {
assertTrue("chunk " + index, cache.isChunkCached(index & 31, index >> 5));
}
assertTrue(cache.isRegionCached(0, 0));
}
@Test
public void aRegionIsOnlyReportedCachedOnceEveryChunkInItLanded() throws Exception {
PregenCache cache = PregenCache.create(directory);
AtomicInteger seenComplete = new AtomicInteger();
runAll(thread -> () -> {
for (int index = thread; index < 1023; index += THREADS) {
cache.cacheChunk(index & 31, index >> 5);
if (cache.isRegionCached(0, 0)) {
seenComplete.incrementAndGet();
}
}
return null;
});
assertEquals(0, seenComplete.get());
assertFalse(cache.isRegionCached(0, 0));
cache.cacheChunk(1023 & 31, 1023 >> 5);
assertTrue(cache.isRegionCached(0, 0));
}
@Test
public void concurrentReadsAndWritesNeverInventACachedChunk() throws Exception {
PregenCache cache = PregenCache.create(directory);
AtomicInteger falsePositives = new AtomicInteger();
runAll(thread -> () -> {
if (thread % 2 == 0) {
for (int index = 0; index < 2048; index++) {
cache.cacheChunk(index, 0);
}
return null;
}
for (int index = 0; index < 2048; index++) {
if (cache.isChunkCached(index, 7000)) {
falsePositives.incrementAndGet();
}
}
return null;
});
assertEquals(0, falsePositives.get());
}
@Test
public void aConcurrentWriteAndFlushRoundTripsThroughDiskWithoutLosingChunks() throws Exception {
PregenCache cache = PregenCache.create(directory);
runAll(thread -> () -> {
for (int index = 0; index < 256; index++) {
cache.cacheChunk((thread * 256) + index, 4);
if (index % 64 == 0) {
cache.write();
}
}
return null;
});
cache.write();
PregenCache reopened = PregenCache.create(directory);
for (int thread = 0; thread < THREADS; thread++) {
for (int index = 0; index < 256; index++) {
int x = (thread * 256) + index;
assertTrue("chunk " + x + " lost across a flush", reopened.isChunkCached(x, 4));
}
}
}
@Test
public void plateEvictionUnderConcurrencyStillPersistsEveryChunk() throws Exception {
PregenCacheImpl cache = new PregenCacheImpl(directory, 1);
int platesPerThread = 6;
runAll(thread -> () -> {
for (int plate = 0; plate < platesPerThread; plate++) {
cache.cacheChunk(plate * 1024, thread);
}
return null;
});
cache.write();
PregenCache reopened = PregenCache.create(directory);
for (int thread = 0; thread < THREADS; thread++) {
for (int plate = 0; plate < platesPerThread; plate++) {
assertTrue("chunk " + (plate * 1024) + "," + thread + " lost to eviction",
reopened.isChunkCached(plate * 1024, thread));
}
}
}
private void runAll(ThreadTask task) throws Exception {
CyclicBarrier barrier = new CyclicBarrier(THREADS);
List<Future<Void>> futures = new ArrayList<>(THREADS);
for (int thread = 0; thread < THREADS; thread++) {
Callable<Void> body = task.forThread(thread);
futures.add(pool.submit(() -> {
barrier.await(30, TimeUnit.SECONDS);
return body.call();
}));
}
for (Future<Void> future : futures) {
future.get(60, TimeUnit.SECONDS);
}
}
private static void deleteRecursively(File file) {
File[] children = file.listFiles();
if (children != null) {
for (File child : children) {
deleteRecursively(child);
}
}
file.delete();
}
private interface ThreadTask {
Callable<Void> forThread(int thread);
}
}
@@ -0,0 +1,43 @@
package art.arcane.iris.core.pregenerator.cache;
import org.junit.Test;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
public class PregenCacheDefaultsTest {
@Test
public void anAbsentDirectoryResolvesToTheSharedEmptyCache() {
assertSame(PregenCache.EMPTY, PregenCache.create(null));
}
@Test
public void theEmptyCacheNeverClaimsAChunkItWasToldToCache() {
PregenCache.EMPTY.cacheChunk(3, -7);
PregenCache.EMPTY.cacheChunk(0, 0);
assertFalse(PregenCache.EMPTY.isChunkCached(3, -7));
assertFalse(PregenCache.EMPTY.isChunkCached(0, 0));
}
@Test
public void theEmptyCacheNeverClaimsARegionItWasToldToCache() {
PregenCache.EMPTY.cacheRegion(-2, 5);
assertFalse(PregenCache.EMPTY.isRegionCached(-2, 5));
}
@Test
public void theEmptyCacheSurvivesMaintenanceCalls() {
PregenCache.EMPTY.write();
PregenCache.EMPTY.trim(0L);
PregenCache.EMPTY.trim(60_000L);
assertFalse(PregenCache.EMPTY.isChunkCached(0, 0));
}
@Test
public void aCacheIsItsOwnSynchronousView() {
assertSame(PregenCache.EMPTY, PregenCache.EMPTY.sync());
}
}
@@ -3,7 +3,9 @@ package art.arcane.iris.core.pregenerator.cache;
import art.arcane.iris.engine.framework.MeteredCache;
import art.arcane.iris.engine.framework.PreservationRegistry;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.testsupport.PlatformLeakGuard;
import org.junit.After;
import org.junit.ClassRule;
import org.junit.Test;
import java.io.File;
@@ -16,6 +18,9 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class PregenCacheExecutorRegistrationTest {
@ClassRule
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
private static final class RecordingPreservation implements PreservationRegistry {
private final List<ExecutorService> executors = new CopyOnWriteArrayList<>();
@@ -1,17 +1,18 @@
package art.arcane.iris.core.pregenerator.methods;
import art.arcane.iris.core.pregenerator.PregenAdmissionGate;
import org.junit.Test;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
@@ -101,13 +102,13 @@ public class AsyncPregenMethodConcurrencyCapTest {
@Test
public void closeDrainWaitsPastWarningIntervalsUntilEveryPermitReturns() throws Exception {
Semaphore semaphore = new Semaphore(0);
PregenAdmissionGate gate = new PregenAdmissionGate(2, 5L, System::currentTimeMillis);
assertNotNull(gate.admit(() -> false, () -> false));
assertNotNull(gate.admit(() -> false, () -> false));
AtomicInteger warnings = new AtomicInteger();
ExecutorService executor = Executors.newSingleThreadExecutor();
try {
Future<Boolean> drain = executor.submit(() -> AsyncPregenMethod.awaitDrain(
semaphore,
2,
Future<Boolean> drain = executor.submit(() -> gate.awaitDrain(
10L,
TimeUnit.MILLISECONDS,
warnings::incrementAndGet
@@ -116,11 +117,12 @@ public class AsyncPregenMethodConcurrencyCapTest {
while (warnings.get() == 0) {
Thread.onSpinWait();
}
semaphore.release(2);
gate.release();
gate.release();
assertFalse(drain.get(1L, TimeUnit.SECONDS));
assertTrue(warnings.get() > 0);
assertEquals(0, semaphore.availablePermits());
assertEquals(2, gate.availablePermits());
} finally {
executor.shutdownNow();
}
@@ -1,144 +0,0 @@
package art.arcane.iris.core.pregenerator.methods;
import org.junit.Test;
import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class PregenMantleCleanupContractTest {
@Test
public void normalEngineCleanupRetriesCandidatesCoveredByTheNewestRealChunk() throws IOException {
String source = read("art/arcane/iris/engine/framework/Engine.java");
String cleanup = method(source, "default void cleanupMantleChunk(int x, int z)");
assertTrue(cleanup.contains("cleanupChunksCoveredBy("));
assertTrue(cleanup.contains("EngineMantle.ChunkCleanupCallback.NONE"));
assertFalse(cleanup.contains(".cleanupChunk(x, z)"));
}
@Test
public void asyncPregenReportsOnlyCleanupCallbacksFromCoveredTargets() throws IOException {
String source = read("art/arcane/iris/core/pregenerator/methods/AsyncPregenMethod.java");
String completion = method(source, "private void completeChunk(");
String cleanup = method(source, "private void cleanupMantleChunksCoveredBy(");
assertTrue(completion.contains("cleanupMantleChunksCoveredBy(x, z, listener)"));
assertFalse(completion.contains("listener.onChunkCleaned"));
assertTrue(cleanup.contains("cleanupChunksCoveredBy(x, z, true, listener::onChunkCleaned)"));
assertFalse(cleanup.contains("forceCleanupChunk"));
}
@Test
public void finalChunkFlushUsesTargetedPersistenceWithoutFullWorldSave() throws IOException {
String source = read("art/arcane/iris/core/pregenerator/methods/AsyncPregenMethod.java");
String flush = method(source, "private void flushAllRemainingChunks()");
String medievalSource = read("art/arcane/iris/core/pregenerator/methods/MedievalPregenMethod.java");
String medievalFlush = method(medievalSource, "private void unloadAndSaveAllChunks(");
assertTrue(flush.contains("evictRegion(regionKey)"));
assertTrue(flush.contains("INMS.get().flushChunkIO(world)"));
assertFalse(flush.contains("world.save()"));
assertTrue(medievalFlush.contains("i.unload(true)"));
assertTrue(medievalFlush.contains("INMS.get().flushChunkIO(world)"));
assertFalse(medievalFlush.contains("world.save()"));
}
@Test
public void foliaFinalEvictionUsesAwaitableRegionOwnedTasks() throws IOException {
String source = read("art/arcane/iris/core/pregenerator/methods/AsyncPregenMethod.java");
String eviction = method(source, "private CompletableFuture<Void> evictFoliaRegion(");
assertTrue(eviction.contains("J.runRegionFuture("));
assertTrue(eviction.contains("CompletableFuture.allOf("));
assertFalse(eviction.contains("J.runRegion("));
}
@Test
public void shutdownReclaimsMantleBeforeReleasingTheJob() throws IOException {
String source = read("art/arcane/iris/core/pregenerator/IrisPregenerator.java");
String shutdown = method(source, "private void shutdown()");
int generator = shutdown.indexOf("shutdownStep(\"generator\"");
int interruptBoundary = shutdown.indexOf("Thread.interrupted()", generator);
int mantle = shutdown.indexOf("shutdownStep(\"mantle\"");
int protocol = shutdown.indexOf("shutdownStep(\"protocol\"");
int listener = shutdown.indexOf("shutdownStep(\"listener\"");
assertTrue(generator >= 0);
assertTrue(interruptBoundary > generator);
assertTrue(mantle > interruptBoundary);
assertTrue(protocol > mantle);
assertTrue(listener > protocol);
}
@Test
public void directStopDoesNotReleaseTheActiveJobBeforeWorkerCleanup() throws IOException {
String source = read("art/arcane/iris/core/gui/PregeneratorJob.java");
String stop = method(source, "public void stop()");
String close = method(source, "public void onClose()");
assertTrue(stop.contains("requestStop()"));
assertFalse(stop.contains("instance.compareAndSet"));
assertTrue(close.contains("instance.compareAndSet(this, null)"));
}
@Test
public void performanceProfilePreparationRunsOnThePregenWorker() throws IOException {
String source = read("art/arcane/iris/core/gui/PregeneratorJob.java");
String constructor = method(source, "public PregeneratorJob(Configuration configuration)");
String worker = method(source, "private void runWorker(Runnable preparation)");
assertTrue(constructor.contains("runWorker(configuration.preparation())"));
assertBefore(worker, "preparation.run()", "pregenerator.start()");
}
private static String read(String relativePath) throws IOException {
Path current;
try {
current = Path.of(PregenMantleCleanupContractTest.class.getProtectionDomain()
.getCodeSource()
.getLocation()
.toURI());
} catch (URISyntaxException e) {
throw new IOException("Unable to resolve the core source root", e);
}
while (current != null && !Files.isDirectory(current.resolve("src/main/java"))) {
current = current.getParent();
}
if (current == null) {
throw new IOException("Unable to locate the core source root");
}
return Files.readString(current.resolve("src/main/java").resolve(relativePath));
}
private static String method(String source, String signature) {
int start = source.indexOf(signature);
assertTrue("Missing source contract signature: " + signature, start >= 0);
int openBrace = source.indexOf('{', start);
assertTrue("Missing source contract method body: " + signature, openBrace >= 0);
int depth = 0;
for (int index = openBrace; index < source.length(); index++) {
char current = source.charAt(index);
if (current == '{') {
depth++;
} else if (current == '}') {
depth--;
if (depth == 0) {
return source.substring(start, index + 1);
}
}
}
throw new IllegalArgumentException("Unclosed source contract method: " + signature);
}
private static void assertBefore(String source, String first, String second) {
int firstIndex = source.indexOf(first);
int secondIndex = source.indexOf(second);
assertTrue(firstIndex >= 0);
assertTrue(secondIndex > firstIndex);
}
}
@@ -7,9 +7,11 @@ import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformBlockProperty;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.iris.testsupport.PlatformLeakGuard;
import art.arcane.volmlib.util.json.JSONObject;
import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
import java.util.List;
@@ -20,6 +22,9 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class ExternalBlockSchemaTest {
@ClassRule
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
private static final String QUALIFIED_BLOCK = "craftengine:example/ores/copper";
private IrisPlatform previous;
@@ -8,10 +8,12 @@ import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.iris.testsupport.PlatformLeakGuard;
import art.arcane.volmlib.util.json.JSONArray;
import art.arcane.volmlib.util.json.JSONObject;
import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
@@ -34,6 +36,9 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class IrisCodeWorkspaceTest {
@ClassRule
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@@ -8,6 +8,7 @@ import art.arcane.iris.engine.object.IrisBlockData;
import art.arcane.iris.engine.object.IrisExpression;
import art.arcane.iris.engine.object.IrisHydrology;
import art.arcane.iris.engine.object.IrisRiverPolicy;
import art.arcane.iris.testsupport.PlatformLeakGuard;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.json.JSONArray;
@@ -17,6 +18,7 @@ import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformRegistries;
import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
import java.util.ArrayList;
@@ -30,6 +32,9 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class IrisRiverSchemaTest {
@ClassRule
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
private IrisPlatform previousPlatform;
@Before
@@ -12,11 +12,13 @@ import art.arcane.iris.engine.object.annotations.ArrayType;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.iris.testsupport.PlatformLeakGuard;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.json.JSONObject;
import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
import java.lang.reflect.Field;
@@ -30,6 +32,9 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class IrisStaticObjectSchemaTest {
@ClassRule
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
private IrisPlatform previousPlatform;
@Before
@@ -0,0 +1,395 @@
/*
* Iris is a World Generator for Minecraft Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.core.project;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.IrisRegistrant;
import art.arcane.iris.core.loader.ResourceLoader;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBlockData;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisEntity;
import art.arcane.iris.engine.object.IrisExpression;
import art.arcane.iris.engine.object.IrisGenerator;
import art.arcane.iris.engine.object.IrisHydrology;
import art.arcane.iris.engine.object.IrisImageMap;
import art.arcane.iris.engine.object.IrisJigsawPiece;
import art.arcane.iris.engine.object.IrisLootTable;
import art.arcane.iris.engine.object.IrisObjectPlacement;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.IrisStructure;
import art.arcane.iris.engine.object.IrisStructurePlacement;
import art.arcane.iris.engine.object.IrisVanillaStructureAdjustment;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.LogLevel;
import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.spi.PlatformBiomeWriter;
import art.arcane.iris.spi.PlatformBlockProperty;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.spi.PlatformEntityType;
import art.arcane.iris.spi.PlatformItem;
import art.arcane.iris.spi.PlatformNumericRange;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.iris.spi.PlatformScheduler;
import art.arcane.iris.spi.PlatformStructureHooks;
import art.arcane.iris.spi.PlatformWorld;
import art.arcane.iris.testsupport.PlatformLeakGuard;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.json.JSONArray;
import art.arcane.volmlib.util.json.JSONObject;
import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeSet;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class SchemaBuilderIdentityTest {
@ClassRule
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
private static final String EXPECTED_SCHEMA_DIGEST = "2f47b30c04bf90a5342df92d7335670679176665520527913f3b90ed0cda65d5";
private static final List<Class<?>> SCHEMA_ROOTS = List.of(
IrisDimension.class,
IrisRegion.class,
IrisBiome.class,
IrisGenerator.class,
IrisHydrology.class,
IrisObjectPlacement.class,
IrisStructure.class,
IrisStructurePlacement.class,
IrisJigsawPiece.class,
IrisVanillaStructureAdjustment.class,
IrisEntity.class,
IrisLootTable.class,
IrisBlockData.class,
IrisImageMap.class,
IrisExpression.class);
private IrisPlatform previous;
@Before
public void bindFakePlatform() {
previous = IrisPlatforms.isBound() ? IrisPlatforms.get() : null;
if (previous != null) {
IrisPlatforms.unbind();
}
IrisPlatforms.bind(new FakePlatform());
}
@After
public void restorePlatform() {
IrisPlatforms.unbind();
if (previous != null) {
IrisPlatforms.bind(previous);
}
}
@Test
public void studioSchemaOutputIsByteIdentical() {
StringBuilder canonical = new StringBuilder();
for (Class<?> root : SCHEMA_ROOTS) {
canonical.append(root.getName()).append('\n');
canonical.append(canonicalize(new SchemaBuilder(root, schemaData()).construct()));
canonical.append('\n');
}
assertEquals(EXPECTED_SCHEMA_DIGEST, digest(canonical.toString()));
}
private static String digest(String value) {
MessageDigest sha256;
try {
sha256 = MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException(e);
}
byte[] hash = sha256.digest(value.getBytes(StandardCharsets.UTF_8));
StringBuilder hex = new StringBuilder(hash.length * 2);
for (byte b : hash) {
hex.append(Character.forDigit((b >> 4) & 0xF, 16));
hex.append(Character.forDigit(b & 0xF, 16));
}
return hex.toString();
}
private static String canonicalize(Object value) {
StringBuilder out = new StringBuilder();
appendCanonical(out, value);
return out.toString();
}
private static void appendCanonical(StringBuilder out, Object value) {
if (value instanceof JSONObject object) {
out.append('{');
boolean first = true;
for (String key : new TreeSet<>(object.keySet())) {
if (!first) {
out.append(',');
}
first = false;
out.append('"').append(key).append("\":");
appendCanonical(out, object.get(key));
}
out.append('}');
return;
}
if (value instanceof JSONArray array) {
out.append('[');
for (int index = 0; index < array.length(); index++) {
if (index > 0) {
out.append(',');
}
appendCanonical(out, array.get(index));
}
out.append(']');
return;
}
out.append('<').append(value == null ? "null" : value.getClass().getSimpleName()).append(':');
out.append(String.valueOf(value)).append('>');
}
private static IrisData schemaData() {
IrisData data = mock(IrisData.class);
ResourceLoader<IrisStructure> structureLoader = mock(ResourceLoader.class);
ResourceLoader<IrisJigsawPiece> pieceLoader = mock(ResourceLoader.class);
ResourceLoader<IrisExpression> expressionLoader = mock(ResourceLoader.class);
ResourceLoader<IrisBlockData> blockLoader = mock(ResourceLoader.class);
KMap<Class<? extends IrisRegistrant>, ResourceLoader<? extends IrisRegistrant>> loaders = new KMap<>();
loaders.put(IrisExpression.class, expressionLoader);
when(data.getStructureLoader()).thenReturn(structureLoader);
when(data.getJigsawPieceLoader()).thenReturn(pieceLoader);
when(data.getBlockLoader()).thenReturn(blockLoader);
when(data.getLoaders()).thenReturn(loaders);
when(data.getPossibleSnippets(anyString())).thenReturn(new KList<String>().qadd("alpha").qadd("beta"));
when(structureLoader.getPossibleKeys()).thenReturn(new String[]{"pack/tower", "pack/keep"});
when(pieceLoader.getPossibleKeys()).thenReturn(new String[]{"pack/tower-base"});
when(blockLoader.getPossibleKeys()).thenReturn(new String[]{"pack/mossy_stone"});
when(expressionLoader.getPossibleKeys()).thenReturn(new String[]{"pack/height"});
when(expressionLoader.getFolderName()).thenReturn("expressions");
when(expressionLoader.getResourceTypeName()).thenReturn("Expression");
return data;
}
private static final class FakeRegistries implements PlatformRegistries {
private static final List<PlatformBlockProperty> STONE_PROPERTIES = List.of(
new PlatformBlockProperty("waterlogged", "boolean", Boolean.FALSE, List.of(Boolean.TRUE, Boolean.FALSE), null),
new PlatformBlockProperty("level", "integer", 0, List.of(0, 1, 2),
new PlatformNumericRange(0D, 2D, false, false)));
private static final List<PlatformBlockProperty> LOG_PROPERTIES = List.of(
new PlatformBlockProperty("axis", "string", "y", List.of("x", "y", "z"), null));
@Override
public PlatformBlockState block(String key) {
return null;
}
@Override
public PlatformBlockState blockOrNull(String key) {
return null;
}
@Override
public PlatformBlockState blockOrNull(String key, boolean warn) {
return null;
}
@Override
public PlatformBlockState air() {
return null;
}
@Override
public PlatformBlockState deepSlateOre(PlatformBlockState block, PlatformBlockState ore) {
return null;
}
@Override
public PlatformBiome biome(String key) {
return null;
}
@Override
public PlatformItem item(String key) {
return null;
}
@Override
public PlatformEntityType entity(String key) {
return null;
}
@Override
public List<String> blockKeys() {
return List.of("minecraft:stone", "minecraft:oak_log");
}
@Override
public List<String> biomeKeys() {
return List.of("minecraft:plains", "minecraft:desert", "cool_mod:sky_meadow");
}
@Override
public List<String> specialEntityKeys() {
return List.of("mythicmobs:skeleton_king");
}
@Override
public List<String> structureKeys() {
return List.of("minecraft:monument", "minecraft:stronghold", "cool_mod:sky_temple");
}
@Override
public List<String> itemKeys() {
return List.of("minecraft:stone", "minecraft:diamond_sword", "cool_mod:ruby");
}
@Override
public List<String> entityKeys() {
return List.of("minecraft:zombie", "cool_mod:grizzly_bear");
}
@Override
public List<String> blockTypeKeys() {
return List.of("minecraft:stone", "minecraft:oak_log");
}
@Override
public List<String> enchantmentKeys() {
return List.of("minecraft:sharpness", "cool_mod:vorpal");
}
@Override
public List<String> potionEffectKeys() {
return List.of("minecraft:speed", "minecraft:slow_falling", "sniffer_mod:mega_boost");
}
@Override
public List<String> lootTableKeys() {
return List.of("minecraft:chests/simple_dungeon", "cool_mod:chests/sky_temple");
}
@Override
public Map<String, List<PlatformBlockProperty>> blockStateProperties() {
Map<String, List<PlatformBlockProperty>> properties = new LinkedHashMap<>();
properties.put("minecraft:stone", STONE_PROPERTIES);
properties.put("minecraft:andesite", STONE_PROPERTIES);
properties.put("minecraft:oak_log", LOG_PROPERTIES);
return properties;
}
}
private static final class FakePlatform implements IrisPlatform {
private final PlatformRegistries registries = new FakeRegistries();
@Override
public String platformName() {
return "fake";
}
@Override
public String minecraftVersion() {
return "0.0.0";
}
@Override
public PlatformRegistries registries() {
return registries;
}
@Override
public PlatformScheduler scheduler() {
return null;
}
@Override
public PlatformStructureHooks structureHooks() {
return null;
}
@Override
public PlatformBiomeWriter biomeWriter() {
return null;
}
@Override
public File dataFolder() {
return new File(".");
}
@Override
public File dataFile(String... path) {
return new File(".");
}
@Override
public File pluginJar() {
return new File(".");
}
@Override
public int irisVersionNumber() {
return 0;
}
@Override
public int minecraftVersionNumber() {
return 0;
}
@Override
public void callEvent(Object event) {
}
@Override
public void dispatchConsoleCommand(String command) {
}
@Override
public boolean spawnEntity(PlatformWorld world, String entityKey, double x, double y, double z) {
return false;
}
@Override
public void log(LogLevel level, String message) {
}
@Override
public void msg(String message) {
}
@Override
public void reportError(Throwable error) {
}
}
}
@@ -55,12 +55,14 @@ import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.iris.spi.PlatformScheduler;
import art.arcane.iris.spi.PlatformStructureHooks;
import art.arcane.iris.spi.PlatformWorld;
import art.arcane.iris.testsupport.PlatformLeakGuard;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.json.JSONArray;
import art.arcane.volmlib.util.json.JSONObject;
import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
import java.io.File;
@@ -76,6 +78,9 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class SchemaBuilderParityTest {
@ClassRule
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
private static final List<String> POTION_KEYS = List.of("minecraft:speed", "minecraft:slow_falling", "sniffer_mod:mega_boost");
private static final List<String> ENCHANT_KEYS = List.of("minecraft:sharpness", "cool_mod:vorpal");
private static final List<String> ITEM_KEYS = List.of("minecraft:stone", "minecraft:diamond_sword", "cool_mod:ruby");
@@ -0,0 +1,73 @@
package art.arcane.iris.core.runtime;
import art.arcane.iris.core.IrisStartupValidation;
import art.arcane.iris.testsupport.BukkitTestServer;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
/**
* Attaching the agent and retransforming server classes costs about a second of every boot, and a server
* that never loads an Iris world never needs either. Deferring it only stays safe while a deferred failure
* locks the runtime exactly like a boot-time one did, so the world it was about to generate is refused
* rather than handed to uninstrumented server code.
*/
public class RuntimeInjectionTest {
@Before
public void resetInjection() {
BukkitTestServer.install();
RuntimeInjection.reset();
}
@After
public void clearValidation() {
RuntimeInjection.reset();
IrisStartupValidation.disable();
}
@Test
public void anInstallWithoutAUsableNmsBindingIsRefusedAndNamesTheReason() {
RuntimeInjection.Outcome outcome = RuntimeInjection.install();
assertFalse(outcome.installed());
assertEquals(RuntimeInjection.Failure.NO_BINDING, outcome.failure());
assertTrue(outcome.lockReason(), outcome.lockReason().contains("NMS runtime"));
}
@Test
public void theOutcomeIsResolvedOnceUntilReset() {
RuntimeInjection.Outcome first = RuntimeInjection.install();
assertSame(first, RuntimeInjection.install());
RuntimeInjection.reset();
assertFalse(RuntimeInjection.install() == first);
}
@Test
public void aDeferredInstallFailureLocksTheRuntimeWithItsReason() {
IrisStartupValidation.begin();
IrisStartupValidation.markDatapacksReady();
IrisStartupValidation.markPacksReady();
IrisStartupValidation.markRuntimeReady();
assertTrue(IrisStartupValidation.isReady());
RuntimeInjection.installIfDeferred();
assertFalse(IrisStartupValidation.isReady());
assertEquals(RuntimeInjection.install().lockReason(),
IrisStartupValidation.denialReason().orElseThrow());
}
@Test
public void anUninstalledRuntimeReportsItself() {
RuntimeInjection.install();
assertFalse(RuntimeInjection.isInstalled());
}
}
@@ -4,9 +4,6 @@ import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.service.ObjectStudioSaveService;
import org.junit.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import static org.junit.Assert.assertFalse;
@@ -18,32 +15,6 @@ public class StudioEntitySpawningTest {
assertTrue(new IrisSettings.IrisSettingsStudio().isEntitySpawning());
}
@Test
public void studioWorldRulesEnableVanillaMobSpawning() throws IOException {
String source = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/runtime/WorldRuntimeControlService.java")).replace("\r\n", "\n");
int helperStart = source.indexOf("static void enableStudioEntitySpawning");
int helperEnd = source.indexOf("public boolean applyNoonTimeLock", helperStart);
String helper = source.substring(helperStart, helperEnd);
assertTrue(source.contains("enableStudioEntitySpawning(world);"));
assertTrue(helper.contains("setBooleanGameRule(world, true, \"DO_MOB_SPAWNING\""));
assertTrue(helper.contains("setBooleanGameRule(world, true, \"DO_TRADER_SPAWNING\""));
assertTrue(helper.contains("setBooleanGameRule(world, true, \"DO_PATROL_SPAWNING\""));
assertTrue(helper.contains("setBooleanGameRule(world, true, \"DO_INSOMNIA\""));
assertTrue(helper.contains("setBooleanGameRule(world, true, \"DO_WARDEN_SPAWNING\""));
assertFalse(helper.contains("setBooleanGameRule(world, false, \"DO_MOB_SPAWNING\""));
}
@Test
public void standardStudioDefaultsToSpectatorMode() throws IOException {
String source = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/runtime/StudioOpenCoordinator.java")).replace("\r\n", "\n");
assertTrue(source.contains("request.openKind().teleportThroughStandardEntry()"));
assertTrue(source.contains("GameMode.SPECTATOR"));
}
@Test
public void objectStudioDoesNotCancelEntitySpawns() {
boolean hasSpawnCanceller = Arrays.stream(ObjectStudioSaveService.class.getDeclaredMethods())
@@ -5,9 +5,6 @@ import art.arcane.iris.core.tools.IrisCreator;
import org.junit.Test;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -86,210 +83,4 @@ public class StudioOpenCoordinatorOpenKindTest {
assertEquals(StudioOpenCoordinator.StudioOpenKind.JIGSAW, request.openKind());
assertTrue(request.requestedAtNanos() > 0L);
}
@Test
public void nativeTeleportWaitsForCompletionWithoutAnArrivalDeadline() throws Exception {
String source = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/runtime/StudioOpenCoordinator.java"))
.replace("\r\n", "\n");
int methodStart = source.indexOf("private void executeOpen(");
int delegation = source.indexOf("WorldRuntimeControlService.get().teleportInMode(", methodStart);
int completion = source.indexOf("nativeTeleportFuture.get();", delegation);
assertTrue(delegation >= 0);
assertTrue(completion > delegation);
assertFalse(source.substring(methodStart, completion).contains("orTimeout("));
assertFalse(source.substring(methodStart, completion).contains("deadlineNanos"));
}
@Test
public void activeStudioTeleportIsSerializedWithoutAnArrivalDeadline() throws Exception {
String coordinator = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/runtime/StudioOpenCoordinator.java"))
.replace("\r\n", "\n");
String service = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/service/StudioSVC.java"))
.replace("\r\n", "\n");
int coordinatorStart = coordinator.indexOf(
"public CompletableFuture<Boolean> teleportPlayerToProject(");
int coordinatorEnd = coordinator.indexOf("private void executeOpen", coordinatorStart);
String coordinatorMethod = coordinator.substring(coordinatorStart, coordinatorEnd);
int serviceStart = service.indexOf(
"public CompletableFuture<Boolean> teleportToActiveProject(Player player)");
int serviceEnd = service.indexOf("public void open(VolmitSender", serviceStart);
String serviceMethod = service.substring(serviceStart, serviceEnd);
int transitionAdmission = serviceMethod.indexOf("studioTransitions.submit(() ->");
int projectCapture = serviceMethod.indexOf("IrisProject project = activeProject");
int nativeDelegation = coordinatorMethod.indexOf(
"WorldRuntimeControlService.get().teleportInMode(player, entry, GameMode.SPECTATOR)");
assertTrue(transitionAdmission >= 0);
assertTrue(projectCapture > transitionAdmission);
assertFalse(serviceMethod.contains("orTimeout("));
assertFalse(serviceMethod.contains("deadlineNanos"));
assertTrue(coordinatorMethod.contains(
"WorldRuntimeControlService.get().resolveEntryAnchor(world, provider)"));
assertTrue(coordinatorMethod.contains(
"project.getActiveOpenKind().teleportThroughStandardEntry()"));
assertFalse(coordinatorMethod.contains("requestChunkAsync("));
assertFalse(coordinatorMethod.contains("getHighestBlockYAt("));
assertTrue(nativeDelegation >= 0);
assertFalse(coordinatorMethod.contains("orTimeout("));
assertFalse(coordinatorMethod.contains("deadlineNanos"));
}
@Test
public void standardEntryDelegatesWithoutAnExplicitChunkOrSurfaceLookup() throws Exception {
String source = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/runtime/StudioOpenCoordinator.java"))
.replace("\r\n", "\n");
assertTrue(source.contains("Location entryLocation = entryAnchor;"));
assertFalse(source.contains("prepareStudioEntryChunks("));
assertFalse(source.contains("findStudioEntryLocation"));
assertFalse(source.contains("EntryChunkResolution"));
assertFalse(source.contains("resolveSafeEntry(world, entryAnchor)"));
assertFalse(source.contains("getHighestBlockYAt("));
}
@Test
public void standardEntryBindsSpectatorToTheNativeTeleport() throws Exception {
String source = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/runtime/StudioOpenCoordinator.java"))
.replace("\r\n", "\n");
String runtime = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/runtime/WorldRuntimeControlService.java"))
.replace("\r\n", "\n");
int mode = runtime.indexOf("modeRestore.apply(gameMode)");
int teleport = runtime.indexOf("teleporter.teleport(player, location)", mode);
assertTrue(source.contains("WorldRuntimeControlService.get().teleportInMode("));
int openKind = source.indexOf("request.project().setActiveOpenKind(request.openKind())");
int provider = source.indexOf("request.project().setActiveProvider(provider)", openKind);
assertTrue(openKind >= 0);
assertTrue(provider > openKind);
assertTrue(source.contains("project.setActiveOpenKind(null)"));
assertTrue(mode >= 0);
assertTrue(teleport > mode);
assertTrue(runtime.contains("modeRestore.restore()"));
}
@Test
public void studioTimingSeparatesOrderedLifecyclePhases() throws Exception {
String source = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/runtime/StudioOpenCoordinator.java")).replace("\r\n", "\n");
int previous = -1;
for (String phase : List.of(
"resolve_dimension_and_cleanup",
"create_world_total",
"apply_world_rules",
"prepare_generator",
"resolve_entry_anchor",
"prepare_structure_rings",
"prepare_generation_caches",
"teleport_standard_entry",
"finalize_open")) {
int current = source.indexOf("\"" + phase + "\"", previous + 1);
assertTrue("Missing or out-of-order Studio timing phase " + phase, current > previous);
previous = current;
}
assertTrue(source.contains("long openStart = System.nanoTime();"));
}
@Test
public void structureStateCompletesBeforeImmediateNativeTeleport() throws Exception {
String source = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/runtime/StudioOpenCoordinator.java")).replace("\r\n", "\n");
int executeOpen = source.indexOf("private void executeOpen(");
int completionCall = source.indexOf("endStudioEntryBootstrap(entryWorld, entryProvider)", executeOpen);
int completionAwait = source.indexOf("entryBootstrap.get(", completionCall);
int cacheAwait = source.indexOf("irisEngine.awaitGenerationCacheWarm()", completionAwait);
int teleportStart = source.indexOf("WorldRuntimeControlService.get().teleportInMode(", cacheAwait);
int finalizeOpen = source.indexOf(
"updateStage(request, \"finalize_open\", 1.00D)", teleportStart);
int futureComplete = source.indexOf(
"future.complete(new StudioOpenResult(world, entryLocation))", finalizeOpen);
int methodStart = source.indexOf(
"private void endStudioEntryBootstrap(World world, PlatformChunkGenerator provider)");
int methodEnd = source.indexOf("private void abandonStudioEntryBootstrap", methodStart);
String method = source.substring(methodStart, methodEnd);
int scheduled = method.indexOf("J.sfut(() ->");
int claim = method.indexOf("activationClaim.compareAndSet(true, false)");
int activation = method.indexOf("INMS.get().completeStudioStructureBootstrap(world)");
int gateRelease = method.indexOf("bukkitGenerator::endStudioEntryBootstrap");
assertTrue(completionCall >= 0);
assertTrue(completionAwait > completionCall);
assertTrue(cacheAwait > completionAwait);
assertTrue(teleportStart > cacheAwait);
assertTrue(finalizeOpen > teleportStart);
assertTrue(futureComplete > finalizeOpen);
assertFalse(source.contains("requestChunkAsync("));
assertTrue(scheduled >= 0);
assertTrue(claim > scheduled);
assertTrue(activation > claim);
int ringCompletion = method.indexOf("thenCompose(nativeActivation -> nativeActivation)");
assertTrue(ringCompletion > activation);
assertTrue(gateRelease > ringCompletion);
assertTrue(source.contains("abandonStudioEntryBootstrap(world, e);"));
}
@Test
public void failedOpenMarshalsRetainedStateAbandonmentToTheServerScheduler() throws Exception {
String source = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/runtime/StudioOpenCoordinator.java")).replace("\r\n", "\n");
int methodStart = source.indexOf(
"private void abandonStudioEntryBootstrap(World world, Throwable failure)");
int methodEnd = source.indexOf("private CompletableFuture<Void> cleanupFailedOpen", methodStart);
String method = source.substring(methodStart, methodEnd);
assertTrue(method.contains("if (J.isPrimaryThread())"));
assertTrue(method.contains("CompletableFuture<Void> abandonment = J.sfut("));
assertTrue(method.contains("abandonment.get(STUDIO_STRUCTURE_ACTIVATION_TIMEOUT_SECONDS"));
}
@Test
public void queuedRestartDefersFailedOpenCleanupWithoutAcquiringALiveCloseLease() throws Exception {
String source = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/runtime/StudioOpenCoordinator.java")).replace("\r\n", "\n");
int openCatch = source.indexOf("} catch (Throwable e) {");
int restartCheck = source.indexOf(
".active(LifecycleOperationCoordinator.Domain.SERVER_LIFECYCLE)",
openCatch);
int restartDeferral = source.indexOf(
"deferFailedOpenCleanupToRestart(",
restartCheck);
int liveCleanup = source.indexOf("cleanupFailedOpen(", restartDeferral);
int methodStart = source.indexOf(
"private void deferFailedOpenCleanupToRestart(",
restartDeferral);
int methodEnd = source.indexOf("private boolean transientWorldStorageExists", methodStart);
String method = source.substring(methodStart, methodEnd);
assertTrue(restartCheck > openCatch);
assertTrue(restartDeferral > restartCheck);
assertTrue(liveCleanup > restartDeferral);
assertTrue(method.contains("queueStartupCleanup("));
assertFalse(method.contains("closeWorldCoordinated("));
}
@Test
public void openFinalizerReturnsToTheServerThreadBeforeCompletion() throws Exception {
String source = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/runtime/StudioOpenCoordinator.java")).replace("\r\n", "\n");
int finalizerCall = source.indexOf("runOpenFinalizer(request.onDone(), world);");
int futureCompletion = source.indexOf(
"future.complete(new StudioOpenResult(world, entryLocation))", finalizerCall);
int methodStart = source.indexOf(
"private void runOpenFinalizer(Consumer<World> finalizer, World world)");
int methodEnd = source.indexOf("private long elapsedMillis", methodStart);
String method = source.substring(methodStart, methodEnd);
assertTrue(finalizerCall >= 0);
assertTrue(futureCompletion > finalizerCall);
assertTrue(method.contains("if (J.isPrimaryThread())"));
assertTrue(method.contains("J.sfut(() -> finalizer.accept(world))"));
assertTrue(method.contains(
"completion.get(STUDIO_STRUCTURE_ACTIVATION_TIMEOUT_SECONDS, TimeUnit.SECONDS)"));
}
}
@@ -2,12 +2,9 @@ package art.arcane.iris.core.runtime;
import org.junit.Test;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class StudioOpenCoordinatorSpawnStuckRegressionTest {
@Test
@@ -30,25 +27,4 @@ public class StudioOpenCoordinatorSpawnStuckRegressionTest {
.anyMatch(m -> m.getName().equals("waitForEntryChunk"));
assertFalse("waitForEntryChunk retry loop must be removed", found);
}
@Test
public void entryTeleportDoesNotIssueASecondChunkRequest() throws Exception {
String source = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/runtime/StudioOpenCoordinator.java")).replace("\r\n", "\n");
assertTrue(source.contains("WorldRuntimeControlService.get().teleportInMode("));
assertFalse(source.contains("prepareStudioEntryChunks("));
assertFalse(source.contains("requestChunkAsync("));
}
@Test
public void foliaEntryPathNeverReadsTerrainOnARegionThread() throws Exception {
String source = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/runtime/StudioOpenCoordinator.java")).replace("\r\n", "\n");
assertTrue(source.contains("resolveEntryAnchor(world, provider)"));
assertFalse(source.contains("findStudioEntryLocation"));
assertFalse(source.contains("getHighestBlockYAt("));
assertFalse(source.contains("resolveSafeEntry(world, entryAnchor)"));
}
}
@@ -2,7 +2,10 @@ package art.arcane.iris.core.runtime;
import art.arcane.iris.core.lifecycle.CapabilitySnapshot;
import art.arcane.iris.core.lifecycle.ServerFamily;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.testsupport.BukkitTestServer;
import org.mockito.ArgumentCaptor;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.junit.Before;
@@ -15,7 +18,11 @@ import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.contains;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
public class WorldRuntimeControlServiceTimeLockTest {
@Before
@@ -47,6 +54,32 @@ public class WorldRuntimeControlServiceTimeLockTest {
assertTrue(setTimeCalled.get());
}
/**
* A runtime that refuses the clock write used to leave a single debug line with no trace, so a world
* stuck at night looked like a pack setting rather than a rejected reflective call.
*/
@Test
public void reportsTheFailureWhenTheRuntimeClockSetterThrows() throws Exception {
IrisPlatform previousPlatform = IrisPlatforms.isBound() ? IrisPlatforms.get() : null;
IrisPlatforms.unbind();
IrisPlatform capturingPlatform = mock(IrisPlatform.class);
IrisPlatforms.bind(capturingPlatform);
try {
World world = createWorldProxy("rejecting-clock", false, new AtomicBoolean(false), new AtomicLong(0L), true);
assertFalse(createService().applyNoonTimeLock(world));
ArgumentCaptor<Throwable> reported = ArgumentCaptor.forClass(Throwable.class);
verify(capturingPlatform).reportError(contains("rejecting-clock"), reported.capture());
assertNotNull(reported.getValue());
} finally {
IrisPlatforms.unbind();
if (previousPlatform != null) {
IrisPlatforms.bind(previousPlatform);
}
}
}
@Test
public void appliesTimeLockWhenWorldHasMutableClock() throws Exception {
AtomicBoolean setTimeCalled = new AtomicBoolean(false);
@@ -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
@@ -143,8 +147,8 @@ public class JigsawStudioGraphEditorResizeTest {
source.setUnsigned(4, 1, 1, stone);
IrisJigsawPiece piece = endPiece(IrisDirection.EAST_POSITIVE_X, new IrisPosition(6, 1, 2));
JigsawStudioGraphEditor.PlanarPieceObjectResize result =
JigsawStudioGraphEditor.resizePlanarPieceObject(
JigsawStudioObjectResizer.PlanarPieceObjectResize result =
JigsawStudioObjectResizer.resizePlanarPieceObject(
source,
piece,
JigsawPlanarArchetype.END,
@@ -171,7 +175,7 @@ public class JigsawStudioGraphEditorResizeTest {
IOException failure = assertThrows(
IOException.class,
() -> JigsawStudioGraphEditor.resizePlanarPieceObject(
() -> JigsawStudioObjectResizer.resizePlanarPieceObject(
source,
piece,
JigsawPlanarArchetype.BLANK,
@@ -195,7 +199,7 @@ public class JigsawStudioGraphEditorResizeTest {
IOException failure = assertThrows(
IOException.class,
() -> JigsawStudioGraphEditor.resizePlanarPieceObject(
() -> JigsawStudioObjectResizer.resizePlanarPieceObject(
source,
piece,
JigsawPlanarArchetype.END,
@@ -10,6 +10,7 @@ import art.arcane.iris.core.structure.authoring.StructureWriteMode;
import art.arcane.iris.core.structure.authoring.StructureWriteResult;
import art.arcane.iris.engine.object.IrisJigsawThemeSet;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.testsupport.DurabilityMode;
import art.arcane.iris.util.common.math.IrisBlockVector;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
@@ -40,6 +41,9 @@ import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class JigsawStudioPersistenceEditorsTest {
@ClassRule
public static final DurabilityMode DURABILITY = DurabilityMode.relaxed();
private static final String PLANAR_KEY = "studio/planar";
private static final String SPATIAL_KEY = "studio/spatial";
@@ -9,6 +9,7 @@ import art.arcane.iris.engine.framework.structure.StructureGraphCompilation;
import art.arcane.iris.engine.framework.structure.StructureResourceBundleGraphCompiler;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.engine.object.IrisPosition;
import art.arcane.iris.testsupport.DurabilityMode;
import art.arcane.iris.util.common.math.IrisBlockVector;
import art.arcane.volmlib.util.math.RNG;
import com.google.gson.JsonObject;
@@ -35,6 +36,9 @@ import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
public class JigsawStudioProjectCreatorTest {
@ClassRule
public static final DurabilityMode DURABILITY = DurabilityMode.relaxed();
private static final JigsawStudioProjectCreator.Options DEFAULT_PLANAR_OPTIONS =
new JigsawStudioProjectCreator.Options(
"settlement/test",
@@ -0,0 +1,42 @@
package art.arcane.iris.core.safeguard;
import org.junit.Test;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
/**
* A boot that ends in Danger Mode used to say so only in the banner. The server then ran on with every
* configured Iris world bound to a generator that throws on the first chunk, and nothing after the banner
* said that out loud. {@code refuseVanillaFallback} never covers this case because it only runs when enable
* itself fails.
*/
public class RuntimeLockNoticeTest {
@Test
public void aReadyRuntimeSaysNothing() {
assertTrue(RuntimeLockNotice.compose(null, true).isEmpty());
assertTrue(RuntimeLockNotice.compose(" ", true).isEmpty());
}
@Test
public void aLockedRuntimeWithIrisWorldsNamesTheReasonAndWhatItBlocks() {
List<String> lines = RuntimeLockNotice.compose("Iris runtime injection failed.", true);
assertEquals(2, lines.size());
assertEquals("Iris enabled with a locked runtime: Iris runtime injection failed.", lines.getFirst());
assertEquals("Every configured Iris world is generation-locked and refuses to generate terrain"
+ " until this is resolved and the server restarts.", lines.get(1));
}
@Test
public void aLockedRuntimeWithoutIrisWorldsSaysOnlyWhatIsBlocked() {
List<String> lines = RuntimeLockNotice.compose("Iris runtime injection failed.", false);
assertEquals(2, lines.size());
assertEquals("Iris enabled with a locked runtime: Iris runtime injection failed.", lines.getFirst());
assertEquals("This server has no Iris world storage, so nothing is generation-locked;"
+ " world creation and player login stay refused until this is resolved.", lines.get(1));
}
}
@@ -0,0 +1,87 @@
package art.arcane.iris.core.safeguard;
import art.arcane.iris.core.safeguard.task.CheckResult;
import art.arcane.iris.core.safeguard.task.Diagnostic;
import art.arcane.iris.core.safeguard.task.Task;
import art.arcane.iris.core.safeguard.task.Tasks;
import art.arcane.iris.testsupport.BukkitTestServer;
import org.bukkit.Bukkit;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.PluginManager;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
/**
* The incompatibility check used to ask the plugin manager for the exact strings "dynmap" and "Stratos".
* Both plugins register under a different case than that - Dynmap's plugin.yml name is "dynmap" on some
* builds and "Dynmap" on others - so the check silently passed on the servers it exists for.
*/
public class TasksIncompatibilityTest {
private PluginManager pluginManager;
@Before
public void installServer() {
BukkitTestServer.install();
pluginManager = Bukkit.getServer().getPluginManager();
doReturn(new Plugin[0]).when(pluginManager).getPlugins();
}
@After
public void clearPlugins() {
doReturn(new Plugin[0]).when(pluginManager).getPlugins();
}
@Test
public void anIncompatiblePluginIsFoundWhateverCaseItRegistersUnder() {
doReturn(new Plugin[]{namedPlugin("DynMap")}).when(pluginManager).getPlugins();
CheckResult result = incompatibilities().run();
assertEquals(Mode.WARNING, result.mode());
assertTrue(headings(result).toString(), headings(result).contains("Dynmap"));
}
@Test
public void aSecondIncompatiblePluginIsReportedTogetherWithTheFirst() {
doReturn(new Plugin[]{namedPlugin("stratos"), namedPlugin("dynmap")}).when(pluginManager).getPlugins();
CheckResult result = incompatibilities().run();
assertEquals(Mode.WARNING, result.mode());
assertTrue(headings(result).toString(), headings(result).containsAll(List.of("Dynmap", "Stratos")));
}
@Test
public void aCleanPluginSetIsStable() {
doReturn(new Plugin[]{namedPlugin("BlueMap")}).when(pluginManager).getPlugins();
assertEquals(Mode.STABLE, incompatibilities().run().mode());
}
private static Task incompatibilities() {
for (Task task : Tasks.getTasks()) {
if ("incompatibilities".equals(task.getId())) {
return task;
}
}
throw new IllegalStateException("The incompatibilities check is missing from the safeguard task list");
}
private static List<String> headings(CheckResult result) {
return result.diagnostics().stream().map(Diagnostic::getMessage).toList();
}
private static Plugin namedPlugin(String name) {
Plugin plugin = mock(Plugin.class);
doReturn(name).when(plugin).getName();
return plugin;
}
}
@@ -0,0 +1,79 @@
package art.arcane.iris.core.safeguard;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.safeguard.task.CheckResult;
import art.arcane.iris.core.safeguard.task.Diagnostic;
import art.arcane.iris.core.safeguard.task.Task;
import art.arcane.iris.core.safeguard.task.Tasks;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.testsupport.BukkitTestServer;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
/**
* With injection deferred the injection check has nothing to verify yet, so it must say so and stay Stable
* rather than reporting a Danger the server has not actually hit. Turning the setting on brings the boot
* verification back.
*/
public class TasksInjectionDeferralTest {
private IrisPlatform previousPlatform;
private IrisSettings previousSettings;
@Before
public void installServerAndSettings() {
BukkitTestServer.install();
previousPlatform = IrisPlatforms.isBound() ? IrisPlatforms.get() : null;
IrisPlatforms.unbind();
IrisPlatforms.bind(mock(IrisPlatform.class));
previousSettings = IrisSettings.settings;
IrisSettings.settings = new IrisSettings();
}
@After
public void restore() {
IrisSettings.settings = previousSettings;
IrisPlatforms.unbind();
if (previousPlatform != null) {
IrisPlatforms.bind(previousPlatform);
}
}
@Test
public void injectionIsDeferredByDefaultAndSaysSo() {
assertTrue("deferred injection is the default", !IrisSettings.settings.getGeneral().isEagerRuntimeInjection());
CheckResult result = injection().run();
assertEquals(Mode.STABLE, result.mode());
List<String> lines = result.diagnostics().stream().map(Diagnostic::getMessage).toList();
assertTrue(lines.toString(), lines.contains("Runtime Injection"));
assertTrue(lines.toString(), lines.stream().anyMatch(line -> line.contains("general.eagerRuntimeInjection")));
}
@Test
public void theEagerSettingBringsBackBootTimeVerification() {
IrisSettings.settings.getGeneral().setEagerRuntimeInjection(true);
CheckResult result = injection().run();
assertEquals(Mode.UNSTABLE, result.mode());
assertTrue(result.lockReason(), result.lockReason().contains("NMS runtime"));
}
private static Task injection() {
for (Task task : Tasks.getTasks()) {
if ("injection".equals(task.getId())) {
return task;
}
}
throw new IllegalStateException("The injection check is missing from the safeguard task list");
}
}
@@ -18,6 +18,7 @@ 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.PlatformLeakGuard;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.board.Board;
import art.arcane.volmlib.util.board.BoardSettings;
@@ -25,6 +26,7 @@ import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.bukkit.event.player.PlayerChangedWorldEvent;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.AfterClass;
import org.junit.BeforeClass;
@@ -54,6 +56,9 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class BoardSVCSavedBiomeTest {
@ClassRule
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
private static IrisPlatform previousPlatform;
@BeforeClass
@@ -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();
@@ -23,9 +23,6 @@ import org.mockito.MockedStatic;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@@ -84,23 +81,6 @@ public class JigsawStudioLifecycleTest {
CreatureSpawnEvent.SpawnReason.SPAWNER));
}
@Test
public void committedStudioActivationDisablesNaturalMobSpawning() throws IOException {
String source = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/service/JigsawStudioService.java")).replace("\r\n", "\n");
int registerStart = source.indexOf("public void register(");
int commitStart = source.indexOf("public void activationCommitted(", registerStart);
int commitEnd = source.indexOf("public void markChunkGenerated(", commitStart);
int helperStart = source.indexOf("static void disableNaturalStudioSpawning(", commitEnd);
String register = source.substring(registerStart, commitStart);
String commit = source.substring(commitStart, commitEnd);
String helper = source.substring(helperStart);
assertFalse(register.contains("disableNaturalStudioSpawning(world)"));
assertTrue(commit.contains("disableNaturalStudioSpawning(world)"));
assertTrue(helper.contains("setGameRule(GameRules.SPAWN_MOBS, false)"));
}
@Test
public void closeAuthorizationChecksOwnerDirtyStateAndSaveBarrierAtomically() {
assertTrue(JigsawStudioActivation.tryBeginOpen(OWNER));
@@ -132,18 +112,18 @@ public class JigsawStudioLifecycleTest {
assertTrue(service.closeProtectionFailure(request.requestId()).contains("loading a variant"));
assertTrue(session.abortVariantSwitch(switchToken));
assertEquals(JigsawStudioService.SaveStart.STARTED, service.tryBeginSave(request.requestId()));
assertEquals(JigsawStudioSaveLifecycle.SaveStart.STARTED, service.saveLifecycle.tryBeginSave(request.requestId()));
assertEquals(
JigsawStudioService.CloseStart.SAVE_IN_PROGRESS,
service.tryBeginClose(request.requestId(), OWNER, true));
assertTrue(service.closeProtectionFailure(request.requestId()).contains("saving"));
service.finishSave(request.requestId());
service.saveLifecycle.finishSave(request.requestId());
assertEquals(
JigsawStudioService.CloseStart.STARTED,
service.tryBeginClose(request.requestId(), OWNER, true));
assertNull(service.closeProtectionFailure(request.requestId()));
assertEquals(JigsawStudioService.SaveStart.CLOSING, service.tryBeginSave(request.requestId()));
assertEquals(JigsawStudioSaveLifecycle.SaveStart.CLOSING, service.saveLifecycle.tryBeginSave(request.requestId()));
}
@Test
@@ -163,7 +143,7 @@ public class JigsawStudioLifecycleTest {
retry.set(invocation.getArgument(0));
return null;
});
CompletableFuture<Void> readiness = service.awaitCloseForReplacement(
CompletableFuture<Void> readiness = service.saveLifecycle.awaitCloseForReplacement(
request.requestId(), OWNER);
assertFalse(readiness.isDone());
@@ -187,7 +167,7 @@ public class JigsawStudioLifecycleTest {
JigsawStudioService service = new JigsawStudioService();
try (MockedStatic<J> scheduling = mockStatic(J.class)) {
CompletableFuture<Void> readiness = service.awaitCloseForReplacement(
CompletableFuture<Void> readiness = service.saveLifecycle.awaitCloseForReplacement(
request.requestId(), OTHER_OWNER);
assertTrue(readiness.isCompletedExceptionally());
@@ -207,9 +187,9 @@ public class JigsawStudioLifecycleTest {
baseline.put("name", "iris:start");
KMap<String, Object> updated = new KMap<>();
updated.put("name", "iris:hall");
assertTrue(JigsawStudioService.tileSnapshotChanged(baseline, updated));
assertTrue(JigsawStudioTileWatcher.tileSnapshotChanged(baseline, updated));
Class<?> keyType = Class.forName(JigsawStudioService.class.getName() + "$JigsawTileWatchKey");
Class<?> keyType = Class.forName(JigsawStudioTileWatcher.class.getName() + "$JigsawTileWatchKey");
Constructor<?> constructor = keyType.getDeclaredConstructor(
UUID.class, UUID.class, int.class, int.class, int.class);
constructor.setAccessible(true);
@@ -233,7 +213,7 @@ public class JigsawStudioLifecycleTest {
new ConcurrentHashMap<>(),
ConcurrentHashMap.newKeySet(),
new AtomicLong());
Class<?> watchType = Class.forName(JigsawStudioService.class.getName() + "$JigsawTileWatch");
Class<?> watchType = Class.forName(JigsawStudioTileWatcher.class.getName() + "$JigsawTileWatch");
Constructor<?> watchConstructor = watchType.getDeclaredConstructor(
keyType,
studioType,
@@ -251,10 +231,10 @@ public class JigsawStudioLifecycleTest {
baseline,
new AtomicBoolean(),
new AtomicBoolean());
Field watchesField = JigsawStudioService.class.getDeclaredField("jigsawTileWatches");
Field watchesField = JigsawStudioTileWatcher.class.getDeclaredField("jigsawTileWatches");
watchesField.setAccessible(true);
@SuppressWarnings("unchecked")
Map<Object, Object> watches = (Map<Object, Object>) watchesField.get(service);
Map<Object, Object> watches = (Map<Object, Object>) watchesField.get(service.tileWatcher);
watches.put(key, watch);
try (MockedStatic<J> scheduling = mockStatic(J.class)) {
@@ -297,55 +277,55 @@ public class JigsawStudioLifecycleTest {
JigsawStudioService.ExportStart.IN_PROGRESS,
service.tryBeginExport(request.requestId(), OWNER));
assertEquals(
JigsawStudioService.SaveStart.EXPORT_OPERATION,
service.tryBeginSave(request.requestId()));
JigsawStudioSaveLifecycle.SaveStart.EXPORT_OPERATION,
service.saveLifecycle.tryBeginSave(request.requestId()));
assertEquals(
JigsawStudioService.CloseStart.OPERATION_IN_PROGRESS,
service.tryBeginClose(request.requestId(), OWNER, false));
assertTrue(service.closeProtectionFailure(request.requestId()).contains("exporting"));
service.finishExport(request.requestId());
assertEquals(JigsawStudioService.SaveStart.STARTED, service.tryBeginSave(request.requestId()));
service.finishSave(request.requestId());
assertEquals(JigsawStudioSaveLifecycle.SaveStart.STARTED, service.saveLifecycle.tryBeginSave(request.requestId()));
service.saveLifecycle.finishSave(request.requestId());
}
@Test
public void immediateSingleAndFamilyDuplicatesWaitForAutosaveThenRunExactlyOnce() {
assertEquals(
JigsawStudioService.DeferredDuplicationReadiness.WAITING_FOR_AUTOSAVE,
JigsawStudioService.deferredDuplicationReadiness(
JigsawStudioToolbelt.DeferredDuplicationReadiness.WAITING_FOR_AUTOSAVE,
JigsawStudioToolbelt.deferredDuplicationReadiness(
true, true, true, false, false));
assertEquals(
JigsawStudioService.DeferredDuplicationReadiness.WAITING_FOR_AUTOSAVE,
JigsawStudioService.deferredDuplicationReadiness(
JigsawStudioToolbelt.DeferredDuplicationReadiness.WAITING_FOR_AUTOSAVE,
JigsawStudioToolbelt.deferredDuplicationReadiness(
true, true, true, true, false));
assertEquals(
JigsawStudioService.DeferredDuplicationReadiness.WAITING_FOR_OPERATION,
JigsawStudioService.deferredDuplicationReadiness(
JigsawStudioToolbelt.DeferredDuplicationReadiness.WAITING_FOR_OPERATION,
JigsawStudioToolbelt.deferredDuplicationReadiness(
true, true, false, true, false));
assertEquals(
JigsawStudioService.DeferredDuplicationReadiness.READY,
JigsawStudioService.deferredDuplicationReadiness(
JigsawStudioToolbelt.DeferredDuplicationReadiness.READY,
JigsawStudioToolbelt.deferredDuplicationReadiness(
true, true, false, false, false));
assertEquals(
JigsawStudioService.DeferredDuplicationReadiness.STALE,
JigsawStudioService.deferredDuplicationReadiness(
JigsawStudioToolbelt.DeferredDuplicationReadiness.STALE,
JigsawStudioToolbelt.deferredDuplicationReadiness(
true, false, false, false, false));
}
@Test
public void initialEvaluationWaitsForCommitAndSchedulesExactlyOnce() {
AtomicLong registrationBeforeCommit = new AtomicLong();
assertFalse(JigsawStudioService.claimInitialEvaluation(registrationBeforeCommit, false));
assertFalse(JigsawStudioEvaluator.claimInitialEvaluation(registrationBeforeCommit, false));
assertEquals(0L, registrationBeforeCommit.get());
assertTrue(JigsawStudioService.claimInitialEvaluation(registrationBeforeCommit, true));
assertTrue(JigsawStudioEvaluator.claimInitialEvaluation(registrationBeforeCommit, true));
assertEquals(1L, registrationBeforeCommit.get());
assertFalse(JigsawStudioService.claimInitialEvaluation(registrationBeforeCommit, true));
assertFalse(JigsawStudioEvaluator.claimInitialEvaluation(registrationBeforeCommit, true));
AtomicLong registrationAfterCommit = new AtomicLong();
assertTrue(JigsawStudioService.claimInitialEvaluation(registrationAfterCommit, true));
assertTrue(JigsawStudioEvaluator.claimInitialEvaluation(registrationAfterCommit, true));
assertEquals(1L, registrationAfterCommit.get());
assertFalse(JigsawStudioService.claimInitialEvaluation(registrationAfterCommit, true));
assertFalse(JigsawStudioEvaluator.claimInitialEvaluation(registrationAfterCommit, true));
}
@Test
@@ -359,9 +339,9 @@ public class JigsawStudioLifecycleTest {
JigsawStudioBay blank = layout.get("workcell/blank");
assertTrue(session.selectedBayId().isEmpty());
assertTrue(JigsawStudioService.selectEnteredWorkcell(session, end, true));
assertTrue(JigsawStudioPlayerContext.selectEnteredWorkcell(session, end, true));
assertEquals(end.stableId(), session.selectedBayId().orElseThrow());
assertFalse(JigsawStudioService.selectEnteredWorkcell(session, blank, false));
assertFalse(JigsawStudioPlayerContext.selectEnteredWorkcell(session, blank, false));
assertEquals(end.stableId(), session.selectedBayId().orElseThrow());
}
@@ -7,6 +7,7 @@ import art.arcane.iris.core.runtime.jigsaw.JigsawStudioPieceRules;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioToolAction;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioToolPayload;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.testsupport.PlatformLeakGuard;
import art.arcane.iris.util.common.scheduling.J;
import org.bukkit.ChatColor;
import org.bukkit.Location;
@@ -15,6 +16,7 @@ import org.bukkit.World;
import org.bukkit.entity.Player;
import org.bukkit.event.player.PlayerChangedWorldEvent;
import org.bukkit.plugin.java.JavaPlugin;
import org.junit.ClassRule;
import org.junit.Test;
import org.mockito.MockedStatic;
@@ -41,6 +43,9 @@ import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
public class JigsawStudioMenuControllerTest {
@ClassRule
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
private static final UUID WORLD_ID = UUID.fromString("22222222-2222-2222-2222-222222222222");
private static final UUID REQUEST_ID = UUID.fromString("77777777-7777-7777-7777-777777777777");
private static final JigsawStudioPieceRules RULES = new JigsawStudioPieceRules(1, 12, 2, 8, false);
@@ -52,13 +57,13 @@ public class JigsawStudioMenuControllerTest {
values.add(index);
}
assertEquals(1, JigsawStudioMenuController.pageCount(0, 28));
assertEquals(1, JigsawStudioMenuController.pageCount(28, 28));
assertEquals(2, JigsawStudioMenuController.pageCount(29, 28));
assertEquals(List.of(0, 1, 2), JigsawStudioMenuController.page(values, -10, 3));
assertEquals(List.of(54, 55, 56), JigsawStudioMenuController.page(values, 999, 3));
assertEquals(1, JigsawStudioMenuFormat.pageCount(0, 28));
assertEquals(1, JigsawStudioMenuFormat.pageCount(28, 28));
assertEquals(2, JigsawStudioMenuFormat.pageCount(29, 28));
assertEquals(List.of(0, 1, 2), JigsawStudioMenuFormat.page(values, -10, 3));
assertEquals(List.of(54, 55, 56), JigsawStudioMenuFormat.page(values, 999, 3));
assertThrows(IllegalArgumentException.class,
() -> JigsawStudioMenuController.pageCount(1, 0));
() -> JigsawStudioMenuFormat.pageCount(1, 0));
}
@Test
@@ -81,7 +86,7 @@ public class JigsawStudioMenuControllerTest {
false,
List.of(active));
assertEquals(ChatColor.GREEN + "Autosaved", JigsawStudioMenuController.workcellStatus(fresh));
assertEquals(ChatColor.GREEN + "Autosaved", JigsawStudioMenuFormat.workcellStatus(fresh));
}
@Test
@@ -92,11 +97,11 @@ public class JigsawStudioMenuControllerTest {
List.of(),
List.of());
assertEquals(Material.JIGSAW, JigsawStudioMenuController.variantMaterial(active));
assertEquals(Material.JIGSAW, JigsawStudioMenuFormat.variantMaterial(active));
assertEquals(Material.EMERALD,
JigsawStudioMenuController.evaluationMaterial(JigsawStudioEvaluationState.VALID));
JigsawStudioMenuFormat.evaluationMaterial(JigsawStudioEvaluationState.VALID));
assertEquals(Material.TARGET,
JigsawStudioMenuController.RuleField.MINIMUM_PLACEMENTS.material());
JigsawStudioMenuFormat.RuleField.MINIMUM_PLACEMENTS.material());
}
@Test
@@ -212,33 +217,33 @@ public class JigsawStudioMenuControllerTest {
assertEquals(
new JigsawStudioCellDimensions(25, 12, 18),
JigsawStudioMenuController.adjustedDimensions(
JigsawStudioMenuFormat.adjustedDimensions(
dimensions,
JigsawStudioMenuController.DimensionAxis.WIDTH,
JigsawStudioMenuFormat.DimensionAxis.WIDTH,
1).orElseThrow());
assertEquals(
new JigsawStudioCellDimensions(24, 20, 18),
JigsawStudioMenuController.adjustedDimensions(
JigsawStudioMenuFormat.adjustedDimensions(
dimensions,
JigsawStudioMenuController.DimensionAxis.HEIGHT,
JigsawStudioMenuFormat.DimensionAxis.HEIGHT,
8).orElseThrow());
assertEquals(
new JigsawStudioCellDimensions(24, 12, 10),
JigsawStudioMenuController.adjustedDimensions(
JigsawStudioMenuFormat.adjustedDimensions(
dimensions,
JigsawStudioMenuController.DimensionAxis.DEPTH,
JigsawStudioMenuFormat.DimensionAxis.DEPTH,
-8).orElseThrow());
assertTrue(JigsawStudioMenuController.adjustedDimensions(
assertTrue(JigsawStudioMenuFormat.adjustedDimensions(
new JigsawStudioCellDimensions(1, 1, 1),
JigsawStudioMenuController.DimensionAxis.WIDTH,
JigsawStudioMenuFormat.DimensionAxis.WIDTH,
-1).isEmpty());
assertTrue(JigsawStudioMenuController.adjustedDimensions(
assertTrue(JigsawStudioMenuFormat.adjustedDimensions(
new JigsawStudioCellDimensions(128, 1, 1),
JigsawStudioMenuController.DimensionAxis.WIDTH,
JigsawStudioMenuFormat.DimensionAxis.WIDTH,
1).isEmpty());
assertThrows(IllegalArgumentException.class, () -> JigsawStudioMenuController.adjustedDimensions(
assertThrows(IllegalArgumentException.class, () -> JigsawStudioMenuFormat.adjustedDimensions(
dimensions,
JigsawStudioMenuController.DimensionAxis.WIDTH,
JigsawStudioMenuFormat.DimensionAxis.WIDTH,
0));
}
@@ -246,21 +251,22 @@ public class JigsawStudioMenuControllerTest {
public void stagesMultipleCellSizeEditsAndAppliesOneRelayoutWithoutClosing() throws Exception {
UUID playerId = UUID.fromString("55555555-5555-5555-5555-555555555555");
Player player = mock(Player.class);
JigsawStudioMenuController.Actions actions = mock(JigsawStudioMenuController.Actions.class);
JigsawStudioMenuActions actions = mock(JigsawStudioMenuActions.class);
JigsawStudioMenuState.Variant active = variant("pieces/corner", true, List.of(), List.of());
JigsawStudioMenuState.Workcell workcell = workcell(active);
JigsawStudioMenuState state = state(JigsawStudioMenuState.Evaluation.pending(), workcell);
JigsawStudioMenuController controller = new JigsawStudioMenuController(
mock(JavaPlugin.class),
actions);
Method resize = JigsawStudioMenuController.class.getDeclaredMethod(
JigsawStudioWorkcellMenu workcellMenu = new JigsawStudioWorkcellMenu(controller);
Method resize = JigsawStudioWorkcellMenu.class.getDeclaredMethod(
"resizeWorkcell",
Player.class,
UUID.class,
String.class,
JigsawStudioMenuController.DimensionAxis.class,
JigsawStudioMenuFormat.DimensionAxis.class,
int.class);
Method apply = JigsawStudioMenuController.class.getDeclaredMethod(
Method apply = JigsawStudioWorkcellMenu.class.getDeclaredMethod(
"applyWorkcellResize",
Player.class,
UUID.class,
@@ -275,18 +281,18 @@ public class JigsawStudioMenuControllerTest {
new JigsawStudioCellDimensions(25, 20, 18))).thenReturn(true);
resize.invoke(
controller,
workcellMenu,
player,
REQUEST_ID,
workcell.stableId(),
JigsawStudioMenuController.DimensionAxis.WIDTH,
JigsawStudioMenuFormat.DimensionAxis.WIDTH,
1);
resize.invoke(
controller,
workcellMenu,
player,
REQUEST_ID,
workcell.stableId(),
JigsawStudioMenuController.DimensionAxis.HEIGHT,
JigsawStudioMenuFormat.DimensionAxis.HEIGHT,
8);
verify(actions, never()).updateWorkcellDimensions(
@@ -299,7 +305,7 @@ public class JigsawStudioMenuControllerTest {
any(Runnable.class),
eq(2))).thenReturn(true);
apply.invoke(controller, player, REQUEST_ID, workcell.stableId());
apply.invoke(workcellMenu, player, REQUEST_ID, workcell.stableId());
verify(actions).updateWorkcellDimensions(
player,
@@ -314,7 +320,7 @@ public class JigsawStudioMenuControllerTest {
JigsawStudioMenuState.Workcell source = workcell(active);
JigsawStudioCellDimensions capacity = new JigsawStudioCellDimensions(31, 19, 27);
JigsawStudioMenuState.Workcell staged = JigsawStudioMenuController.withCapacity(source, capacity);
JigsawStudioMenuState.Workcell staged = JigsawStudioMenuFormat.withCapacity(source, capacity);
assertEquals(capacity, staged.capacity());
assertEquals(source.stableId(), staged.stableId());
@@ -330,12 +336,12 @@ public class JigsawStudioMenuControllerTest {
new JigsawStudioMenuState.ThemeSet("seasonal", 4),
new JigsawStudioMenuState.ThemeSet("variant-2", 2));
assertEquals("variant-3", JigsawStudioMenuController.nextThemeSetKey(themeSets));
assertEquals("variant-1", JigsawStudioMenuController.nextThemeSetKey(List.of()));
assertEquals(Integer.valueOf(12), JigsawStudioMenuController.adjustedPositiveValue(4, 8).orElseThrow());
assertTrue(JigsawStudioMenuController.adjustedPositiveValue(4, -8).isEmpty());
assertEquals("variant-3", JigsawStudioMenuFormat.nextThemeSetKey(themeSets));
assertEquals("variant-1", JigsawStudioMenuFormat.nextThemeSetKey(List.of()));
assertEquals(Integer.valueOf(12), JigsawStudioMenuFormat.adjustedPositiveValue(4, 8).orElseThrow());
assertTrue(JigsawStudioMenuFormat.adjustedPositiveValue(4, -8).isEmpty());
assertThrows(IllegalArgumentException.class,
() -> JigsawStudioMenuController.adjustedPositiveValue(4, 0));
() -> JigsawStudioMenuFormat.adjustedPositiveValue(4, 0));
}
@Test
@@ -344,10 +350,10 @@ public class JigsawStudioMenuControllerTest {
new JigsawStudioMenuState.ThemeSet("variant-1", 3),
new JigsawStudioMenuState.ThemeSet("variant-2", 1));
assertEquals("75.0%", JigsawStudioMenuController.themeSelectionPercent(
assertEquals("75.0%", JigsawStudioMenuFormat.themeSelectionPercent(
themes,
themes.getFirst()));
assertEquals("25.0%", JigsawStudioMenuController.themeSelectionPercent(
assertEquals("25.0%", JigsawStudioMenuFormat.themeSelectionPercent(
themes,
themes.getLast()));
}
@@ -356,47 +362,47 @@ public class JigsawStudioMenuControllerTest {
public void editsPieceRuleFieldsWithinRuntimeBounds() {
assertEquals(
new JigsawStudioPieceRules(2, 12, 2, 8, false),
JigsawStudioMenuController.adjustedRules(
JigsawStudioMenuFormat.adjustedRules(
RULES,
JigsawStudioMenuController.RuleField.MINIMUM_DEPTH,
JigsawStudioMenuFormat.RuleField.MINIMUM_DEPTH,
1).orElseThrow());
assertEquals(
new JigsawStudioPieceRules(1, 17, 2, 8, false),
JigsawStudioMenuController.adjustedRules(
JigsawStudioMenuFormat.adjustedRules(
RULES,
JigsawStudioMenuController.RuleField.MAXIMUM_DEPTH,
JigsawStudioMenuFormat.RuleField.MAXIMUM_DEPTH,
5).orElseThrow());
assertEquals(
new JigsawStudioPieceRules(1, 12, 3, 8, false),
JigsawStudioMenuController.adjustedRules(
JigsawStudioMenuFormat.adjustedRules(
RULES,
JigsawStudioMenuController.RuleField.MINIMUM_PLACEMENTS,
JigsawStudioMenuFormat.RuleField.MINIMUM_PLACEMENTS,
1).orElseThrow());
assertEquals(
new JigsawStudioPieceRules(1, 12, 2, 24, false),
JigsawStudioMenuController.adjustedRules(
JigsawStudioMenuFormat.adjustedRules(
RULES,
JigsawStudioMenuController.RuleField.MAXIMUM_PLACEMENTS,
JigsawStudioMenuFormat.RuleField.MAXIMUM_PLACEMENTS,
16).orElseThrow());
assertEquals(
new JigsawStudioPieceRules(0, 30, 0, 0, false),
JigsawStudioMenuController.adjustedRules(
JigsawStudioMenuFormat.adjustedRules(
new JigsawStudioPieceRules(0, 30, 0, 512, false),
JigsawStudioMenuController.RuleField.MAXIMUM_PLACEMENTS,
JigsawStudioMenuFormat.RuleField.MAXIMUM_PLACEMENTS,
1).orElseThrow());
assertEquals(
new JigsawStudioPieceRules(0, 30, 0, 512, false),
JigsawStudioMenuController.adjustedRules(
JigsawStudioMenuFormat.adjustedRules(
new JigsawStudioPieceRules(0, 30, 0, 0, false),
JigsawStudioMenuController.RuleField.MAXIMUM_PLACEMENTS,
JigsawStudioMenuFormat.RuleField.MAXIMUM_PLACEMENTS,
-1).orElseThrow());
assertTrue(JigsawStudioMenuController.adjustedRules(
assertTrue(JigsawStudioMenuFormat.adjustedRules(
RULES,
JigsawStudioMenuController.RuleField.MINIMUM_DEPTH,
JigsawStudioMenuFormat.RuleField.MINIMUM_DEPTH,
12).isEmpty());
assertEquals(
new JigsawStudioPieceRules(1, 12, 2, 8, true),
JigsawStudioMenuController.withTerminal(RULES, true));
JigsawStudioMenuFormat.withTerminal(RULES, true));
}
@Test
@@ -419,23 +425,23 @@ public class JigsawStudioMenuControllerTest {
JigsawStudioMenuState.Workcell corner = workcell(active, List.of(active, inactive));
JigsawStudioMenuState state = state(JigsawStudioMenuState.Evaluation.pending(), corner);
List<JigsawStudioMenuController.ToolboxTool> tools = JigsawStudioMenuController.toolboxTools(
List<JigsawStudioToolboxMenu.ToolboxTool> tools = JigsawStudioToolboxMenu.toolboxTools(
state,
corner);
JigsawStudioMenuController.ToolboxTool preview = tool(tools, JigsawStudioToolAction.PREVIEW_GRAPH, 0);
JigsawStudioMenuController.ToolboxTool chanceIncrease = tool(
JigsawStudioToolboxMenu.ToolboxTool preview = tool(tools, JigsawStudioToolAction.PREVIEW_GRAPH, 0);
JigsawStudioToolboxMenu.ToolboxTool chanceIncrease = tool(
tools,
JigsawStudioToolAction.ADJUST_VARIANT_CHANCE,
JigsawStudioMenuController.CHANCE_STEP_PERCENTAGE_POINTS);
JigsawStudioMenuController.ToolboxTool delete = tool(
JigsawStudioToolboxMenu.ToolboxTool delete = tool(
tools,
JigsawStudioToolAction.DELETE_VARIANT,
0);
JigsawStudioMenuController.ToolboxTool newThemeSet = tools.stream()
JigsawStudioToolboxMenu.ToolboxTool newThemeSet = tools.stream()
.filter(tool -> tool.payload().action() == JigsawStudioToolAction.DUPLICATE_FAMILY)
.findFirst()
.orElseThrow();
JigsawStudioMenuController.ToolboxTool pieceRules = tools.stream()
JigsawStudioToolboxMenu.ToolboxTool pieceRules = tools.stream()
.filter(tool -> tool.payload().action() == JigsawStudioToolAction.SET_PIECE_RULES)
.findFirst()
.orElseThrow();
@@ -484,7 +490,7 @@ public class JigsawStudioMenuControllerTest {
JigsawStudioMenuState.Evaluation.pending(),
List.of(room));
List<JigsawStudioMenuController.ToolboxTool> tools = JigsawStudioMenuController.toolboxTools(state, room);
List<JigsawStudioToolboxMenu.ToolboxTool> tools = JigsawStudioToolboxMenu.toolboxTools(state, room);
assertTrue(tools.stream().anyMatch(tool -> tool.payload().action()
== JigsawStudioToolAction.RESIZE_WORKCELL));
@@ -514,7 +520,7 @@ public class JigsawStudioMenuControllerTest {
JigsawStudioMenuState.Evaluation.pending(),
List.of(corner));
List<JigsawStudioMenuController.ToolboxTool> tools = JigsawStudioMenuController.toolboxTools(state, corner);
List<JigsawStudioToolboxMenu.ToolboxTool> tools = JigsawStudioToolboxMenu.toolboxTools(state, corner);
assertFalse(state.irisExtended());
assertTrue(tools.stream().noneMatch(tool -> tool.payload().action() == JigsawStudioToolAction.SET_THEME));
@@ -531,7 +537,7 @@ public class JigsawStudioMenuControllerTest {
@Test
public void openMarshalsToThePlayerRegionBeforeReadingStudioState() {
Player player = mock(Player.class);
JigsawStudioMenuController.Actions actions = mock(JigsawStudioMenuController.Actions.class);
JigsawStudioMenuActions actions = mock(JigsawStudioMenuActions.class);
JigsawStudioMenuController controller = new JigsawStudioMenuController(
mock(JavaPlugin.class),
actions);
@@ -565,14 +571,14 @@ public class JigsawStudioMenuControllerTest {
when(world.getUID()).thenReturn(worldId);
when(location.getWorld()).thenReturn(world);
Field menuField = JigsawStudioService.class.getDeclaredField("menuController");
Field menuField = JigsawStudioToolbelt.class.getDeclaredField("menuController");
menuField.setAccessible(true);
menuField.set(service, menu);
menuField.set(service.toolbelt, menu);
IrisServices.register(BoardSVC.class, board);
try (MockedStatic<J> scheduling = mockStatic(J.class)) {
scheduling.when(() -> J.isOwnedByCurrentRegion(player)).thenReturn(true);
service.onPlayerChangedWorld(event);
service.protectionListener.onPlayerChangedWorld(event);
verify(menu).close(player);
verify(board).clearJigsawContext(player);
@@ -644,12 +650,12 @@ public class JigsawStudioMenuControllerTest {
memberships);
}
private JigsawStudioMenuController.ToolboxTool tool(
List<JigsawStudioMenuController.ToolboxTool> tools,
private JigsawStudioToolboxMenu.ToolboxTool tool(
List<JigsawStudioToolboxMenu.ToolboxTool> tools,
JigsawStudioToolAction action,
int amount
) {
Optional<JigsawStudioMenuController.ToolboxTool> match = tools.stream()
Optional<JigsawStudioToolboxMenu.ToolboxTool> match = tools.stream()
.filter(tool -> tool.payload().action() == action && tool.payload().amount() == amount)
.findFirst();
return match.orElseThrow();
@@ -0,0 +1,213 @@
package art.arcane.iris.core.service;
import art.arcane.iris.core.runtime.jigsaw.JigsawPlanarArchetype;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioCellDimensions;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioCompatibilityTarget;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioGraphEditor;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioMode;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioPieceRules;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioPoolEditor;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioProjectCreator;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioStructureEditor;
import art.arcane.iris.core.structure.authoring.StructureHash;
import art.arcane.iris.engine.object.IrisJigsawThemeSet;
import art.arcane.iris.testsupport.DurabilityMode;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Stream;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class JigsawStudioPersistenceIdentityTest {
@ClassRule
public static final DurabilityMode DURABILITY = DurabilityMode.relaxed();
private static final String STRUCTURE_KEY = "studio/identity";
private static final String PIECE_POOL_KEY = "studio/identity/pieces";
private static final String RECORDED_AT_FIELD = "recordedAtEpochMilli";
private static final String SCRIPTED_SESSION_DIGEST =
"318d9338a63a44063c85e59a9e74f869473d6e0f15c362536172f7723d960faa";
@Rule
public final TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void scriptedEditingSessionWritesTheSameProjectAndHistoryBytes() throws Exception {
assertEquals(SCRIPTED_SESSION_DIGEST, digestOf(runScriptedSession("session-one")));
}
@Test
public void repeatedScriptedEditingSessionsAreByteIdentical() throws Exception {
assertEquals(digestOf(runScriptedSession("session-two")), digestOf(runScriptedSession("session-three")));
}
private Path runScriptedSession(String folderName) throws Exception {
Path packRoot = temporaryFolder.newFolder(folderName).toPath();
assertTrue(JigsawStudioProjectCreator.create(
packRoot,
new JigsawStudioProjectCreator.Options(
STRUCTURE_KEY,
JigsawStudioMode.PLANAR_JIGSAW,
JigsawStudioCompatibilityTarget.IRIS_EXTENDED,
new JigsawStudioCellDimensions(16, 16, 16))).successful());
JigsawStudioHistoryStore history = new JigsawStudioHistoryStore(packRoot, STRUCTURE_KEY);
history.append(history.snapshotCurrent(STRUCTURE_KEY + "/end"));
assertTrue(JigsawStudioPoolEditor.updateWeightAtIndex(
packRoot, STRUCTURE_KEY, PIECE_POOL_KEY, 0, STRUCTURE_KEY + "/end", 4).changed());
assertTrue(JigsawStudioPoolEditor.updateChanceAtIndex(
packRoot, STRUCTURE_KEY, PIECE_POOL_KEY, 0, STRUCTURE_KEY + "/end", 0.35D).changed());
history.append(history.snapshotCurrent(STRUCTURE_KEY + "/end"));
JigsawStudioGraphEditor.VariantFamilyCreation family = JigsawStudioGraphEditor.duplicateActiveFamily(
packRoot, STRUCTURE_KEY, planarSources(), "variant-2");
assertTrue(family.writeResult().successful());
assertTrue(JigsawStudioGraphEditor.updatePieceThemes(
packRoot, STRUCTURE_KEY, STRUCTURE_KEY + "/blank", List.of()).successful());
assertTrue(JigsawStudioGraphEditor.updatePieceDisplayName(
packRoot, STRUCTURE_KEY, STRUCTURE_KEY + "/end", "Sealed End").successful());
assertTrue(JigsawStudioGraphEditor.updateRotatable(
packRoot, STRUCTURE_KEY, STRUCTURE_KEY + "/end", false).successful());
assertTrue(JigsawStudioGraphEditor.updatePieceRules(
packRoot,
STRUCTURE_KEY,
STRUCTURE_KEY + "/end",
new JigsawStudioPieceRules(0, 30, 0, 0, true)).successful());
assertTrue(JigsawStudioStructureEditor.updateThemeSets(
packRoot,
STRUCTURE_KEY,
List.of(new IrisJigsawThemeSet("variant-1", 2), new IrisJigsawThemeSet("variant-2", 3)))
.successful());
assertTrue(JigsawStudioStructureEditor.updateLimits(packRoot, STRUCTURE_KEY, 9, 5).successful());
history.append(history.snapshotCurrent(STRUCTURE_KEY + "/end"));
assertTrue(JigsawStudioGraphEditor.createPool(
packRoot, STRUCTURE_KEY, STRUCTURE_KEY + "/spares", PIECE_POOL_KEY).successful());
assertTrue(JigsawStudioGraphEditor.duplicatePiece(
packRoot,
STRUCTURE_KEY,
STRUCTURE_KEY + "/end",
STRUCTURE_KEY + "/variants/end/variant-3").successful());
assertTrue(JigsawStudioPoolEditor.addPiece(
packRoot,
STRUCTURE_KEY,
STRUCTURE_KEY + "/spares",
STRUCTURE_KEY + "/variants/end/variant-3",
6).changed());
assertTrue(JigsawStudioPoolEditor.updateFallback(
packRoot, STRUCTURE_KEY, STRUCTURE_KEY + "/spares", "").changed());
history.append(history.snapshotCurrent(STRUCTURE_KEY + "/end"));
JigsawStudioCellDimensions endCapacity = new JigsawStudioCellDimensions(14, 12, 14);
for (String endVariant : List.of(
STRUCTURE_KEY + "/end",
family.pieceKeysByWorkcell().get(JigsawPlanarArchetype.END.stableId()),
STRUCTURE_KEY + "/variants/end/variant-3")) {
assertTrue(JigsawStudioGraphEditor.resizePieceObject(
packRoot, STRUCTURE_KEY, endVariant, endCapacity).writeResult().successful());
}
assertTrue(JigsawStudioGraphEditor.updatePlanarWorkcellCapacity(
packRoot, STRUCTURE_KEY, JigsawPlanarArchetype.END, endCapacity)
.writeResult().successful());
history.append(history.snapshotCurrent(STRUCTURE_KEY + "/end"));
assertTrue(JigsawStudioGraphEditor.deletePieceVariant(
packRoot,
STRUCTURE_KEY,
family.pieceKeysByWorkcell().get(JigsawPlanarArchetype.CORNER.stableId()))
.writeResult().successful());
JigsawStudioHistoryStore.UndoResult undo = history.undoLatest();
assertTrue(undo.available());
assertTrue(undo.successful());
return packRoot;
}
private static Map<String, String> planarSources() {
Map<String, String> sources = new LinkedHashMap<>();
for (JigsawPlanarArchetype archetype : JigsawPlanarArchetype.values()) {
sources.put(archetype.stableId(), STRUCTURE_KEY + "/" + archetype.name().toLowerCase(Locale.ROOT));
}
return sources;
}
private static String digestOf(Path packRoot) throws IOException {
List<String> entries = new ArrayList<>();
try (Stream<Path> files = Files.walk(packRoot)) {
for (Path file : files.toList()) {
if (!Files.isRegularFile(file)) {
continue;
}
String relative = packRoot.relativize(file).toString().replace('\\', '/');
entries.add(relative + ' ' + StructureHash.sha256(stableContent(file, relative)));
}
}
Collections.sort(entries);
return StructureHash.sha256(String.join("\n", entries).getBytes(StandardCharsets.UTF_8));
}
private static byte[] stableContent(Path file, String relative) throws IOException {
byte[] content = Files.readAllBytes(file);
if (!relative.startsWith(".iris/jigsaw-history/")) {
return content;
}
JsonElement document = JsonParser.parseString(new String(content, StandardCharsets.UTF_8));
return canonicalJson(document).getBytes(StandardCharsets.UTF_8);
}
private static String canonicalJson(JsonElement element) {
if (element.isJsonObject()) {
StringBuilder builder = new StringBuilder("{");
TreeMap<String, JsonElement> members = new TreeMap<>();
for (Map.Entry<String, JsonElement> member : element.getAsJsonObject().entrySet()) {
members.put(member.getKey(), member.getValue());
}
boolean first = true;
for (Map.Entry<String, JsonElement> member : members.entrySet()) {
if (!first) {
builder.append(',');
}
first = false;
builder.append('"').append(member.getKey()).append("\":");
if (RECORDED_AT_FIELD.equals(member.getKey())) {
builder.append('0');
} else {
builder.append(canonicalJson(member.getValue()));
}
}
return builder.append('}').toString();
}
if (element.isJsonArray()) {
StringBuilder builder = new StringBuilder("[");
boolean first = true;
for (JsonElement entry : element.getAsJsonArray()) {
if (!first) {
builder.append(',');
}
first = false;
builder.append(canonicalJson(entry));
}
return builder.append(']').toString();
}
return element.toString();
}
}
@@ -79,10 +79,10 @@ public class JigsawStudioPreviewRendererTest {
10,
IrisObjectRotation.of(0, 0, 0));
PlacedStructurePiece planar = JigsawStudioService.alignPreviewPieces(
PlacedStructurePiece planar = JigsawStudioEvaluator.alignPreviewPieces(
List.of(source),
JigsawStudioMode.PLANAR_JIGSAW).getFirst();
PlacedStructurePiece spatial = JigsawStudioService.alignPreviewPieces(
PlacedStructurePiece spatial = JigsawStudioEvaluator.alignPreviewPieces(
List.of(source),
JigsawStudioMode.SPATIAL_JIGSAW).getFirst();
@@ -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";
@@ -36,6 +36,7 @@ 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.PlatformLeakGuard;
import art.arcane.iris.util.common.data.B;
import art.arcane.iris.util.common.math.IrisBlockVector;
import art.arcane.iris.util.common.plugin.VolmitPlugin;
@@ -72,6 +73,7 @@ import org.bukkit.event.player.PlayerCommandPreprocessEvent;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.event.world.ChunkLoadEvent;
import org.bukkit.event.world.WorldUnloadEvent;
import org.junit.ClassRule;
import org.junit.Test;
import org.mockito.MockedStatic;
@@ -115,6 +117,9 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class JigsawStudioServiceCaptureTest {
@ClassRule
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
@Test
public void shutdownQuiesceIsIdempotentAndResetsOnEnable() throws Exception {
@@ -192,11 +197,11 @@ public class JigsawStudioServiceCaptureTest {
when(state.isCustom()).thenReturn(false);
when(state.nativeHandle()).thenReturn(blockData);
JigsawStudioService.restoreConnectorChunk(
JigsawStudioChunkWriter.restoreConnectorChunk(
world,
workcell,
List.of(renderedConnector),
Map.of(new JigsawStudioService.LocalPosition(1, 2, 3), renderedBlock),
Map.of(new JigsawStudioCapture.LocalPosition(1, 2, 3), renderedBlock),
false);
verify(world).getBlockAt(11, 22, 33);
@@ -224,9 +229,9 @@ public class JigsawStudioServiceCaptureTest {
expandedSpecs,
JigsawStudioVariantCatalog.empty());
assertFalse(JigsawStudioService.layoutGeometryChanged(original, original));
assertTrue(JigsawStudioService.layoutGeometryChanged(original, expanded));
Set<Long> chunks = JigsawStudioService.relayoutChunks(original, expanded);
assertFalse(JigsawStudioGraphMutations.layoutGeometryChanged(original, original));
assertTrue(JigsawStudioGraphMutations.layoutGeometryChanged(original, expanded));
Set<Long> chunks = JigsawStudioGraphMutations.relayoutChunks(original, expanded);
assertTrue(chunks.contains(0L));
assertTrue(chunks.contains(((long) 4 << 32)));
}
@@ -243,8 +248,8 @@ public class JigsawStudioServiceCaptureTest {
dimensions,
new JigsawStudioVariantCatalog(List.of(), false));
assertTrue(JigsawStudioService.canCreateVariants(editable));
assertFalse(JigsawStudioService.canCreateVariants(managed));
assertTrue(JigsawStudioProtection.canCreateVariants(editable));
assertFalse(JigsawStudioProtection.canCreateVariants(managed));
}
@Test
@@ -264,27 +269,27 @@ public class JigsawStudioServiceCaptureTest {
new JigsawStudioPoolMembership("test/pieces", 0, 4, 0.35D),
new JigsawStudioPoolMembership("test/caps", 0, 7, 0.8D)));
assertEquals("", JigsawStudioService.variantCreationSourceFailure(assigned, false));
assertEquals("", JigsawStudioService.variantCreationSourceFailure(assigned, true));
assertTrue(JigsawStudioService.variantCreationSourceFailure(null, false)
assertEquals("", JigsawStudioProtection.variantCreationSourceFailure(assigned, false));
assertEquals("", JigsawStudioProtection.variantCreationSourceFailure(assigned, true));
assertTrue(JigsawStudioProtection.variantCreationSourceFailure(null, false)
.contains("piece create <poolKey> <pieceKey>"));
assertTrue(JigsawStudioService.variantCreationSourceFailure(planarVariant(true, true), false)
assertTrue(JigsawStudioProtection.variantCreationSourceFailure(planarVariant(true, true), false)
.contains("no owned pool membership"));
assertTrue(JigsawStudioService.variantCreationSourceFailure(planarVariant(true, false), true)
assertTrue(JigsawStudioProtection.variantCreationSourceFailure(planarVariant(true, false), true)
.contains("read-only variant"));
}
@Test
public void recognizesVanillaNamespacedAndWorldEditMutationCommands() {
assertTrue(JigsawStudioService.isMutatingCommand("/fill 0 0 0 1 1 1 stone"));
assertTrue(JigsawStudioService.isMutatingCommand("minecraft:setblock 0 0 0 air"));
assertTrue(JigsawStudioService.isMutatingCommand("//paste -a"));
assertTrue(JigsawStudioService.isMutatingCommand("/execute as @s run setblock 0 0 0 stone"));
assertTrue(JigsawStudioService.isMutatingCommand("/function test:build"));
assertTrue(JigsawStudioService.isMutatingCommand("/data merge block 0 0 0 {}"));
assertTrue(JigsawStudioService.isMutatingCommand("/item replace block 0 0 0 container.0 with stone"));
assertFalse(JigsawStudioService.isMutatingCommand("/iris jigsaw status"));
assertFalse(JigsawStudioService.isMutatingCommand("/tp 0 80 0"));
assertTrue(JigsawStudioProtection.isMutatingCommand("/fill 0 0 0 1 1 1 stone"));
assertTrue(JigsawStudioProtection.isMutatingCommand("minecraft:setblock 0 0 0 air"));
assertTrue(JigsawStudioProtection.isMutatingCommand("//paste -a"));
assertTrue(JigsawStudioProtection.isMutatingCommand("/execute as @s run setblock 0 0 0 stone"));
assertTrue(JigsawStudioProtection.isMutatingCommand("/function test:build"));
assertTrue(JigsawStudioProtection.isMutatingCommand("/data merge block 0 0 0 {}"));
assertTrue(JigsawStudioProtection.isMutatingCommand("/item replace block 0 0 0 container.0 with stone"));
assertFalse(JigsawStudioProtection.isMutatingCommand("/iris jigsaw status"));
assertFalse(JigsawStudioProtection.isMutatingCommand("/tp 0 80 0"));
}
@Test
@@ -292,28 +297,28 @@ public class JigsawStudioServiceCaptureTest {
UUID ownerId = UUID.fromString("11111111-1111-1111-1111-111111111111");
UUID otherId = UUID.fromString("22222222-2222-2222-2222-222222222222");
assertTrue(JigsawStudioService.ownerMatches(ownerId, ownerId));
assertTrue(JigsawStudioService.ownerMatches(null, otherId));
assertFalse(JigsawStudioService.ownerMatches(ownerId, otherId));
assertTrue(JigsawStudioService.blocksStudioEdit(ownerId, otherId));
assertFalse(JigsawStudioService.blocksStudioEdit(ownerId, ownerId));
assertFalse(JigsawStudioService.blocksStudioEdit(null, otherId));
assertTrue(JigsawStudioService.blocksMutatingCommand(ownerId, otherId, "//paste -a"));
assertFalse(JigsawStudioService.blocksMutatingCommand(ownerId, ownerId, "//paste -a"));
assertFalse(JigsawStudioService.blocksMutatingCommand(null, otherId, "//paste -a"));
assertFalse(JigsawStudioService.blocksMutatingCommand(ownerId, otherId, "/iris jigsaw status"));
assertFalse(JigsawStudioService.blocksMutatingCommand(ownerId, otherId, "/msg owner hello"));
assertTrue(JigsawStudioService.blocksMutatingCommand(ownerId, otherId, "/execute run say bypass"));
assertTrue(JigsawStudioService.blocksMutatingCommand(ownerId, otherId, "/unknownplugin mutate"));
assertTrue(JigsawStudioService.blocksNonEditableWorkcellMutation(
assertTrue(JigsawStudioProtection.ownerMatches(ownerId, ownerId));
assertTrue(JigsawStudioProtection.ownerMatches(null, otherId));
assertFalse(JigsawStudioProtection.ownerMatches(ownerId, otherId));
assertTrue(JigsawStudioProtection.blocksStudioEdit(ownerId, otherId));
assertFalse(JigsawStudioProtection.blocksStudioEdit(ownerId, ownerId));
assertFalse(JigsawStudioProtection.blocksStudioEdit(null, otherId));
assertTrue(JigsawStudioProtection.blocksMutatingCommand(ownerId, otherId, "//paste -a"));
assertFalse(JigsawStudioProtection.blocksMutatingCommand(ownerId, ownerId, "//paste -a"));
assertFalse(JigsawStudioProtection.blocksMutatingCommand(null, otherId, "//paste -a"));
assertFalse(JigsawStudioProtection.blocksMutatingCommand(ownerId, otherId, "/iris jigsaw status"));
assertFalse(JigsawStudioProtection.blocksMutatingCommand(ownerId, otherId, "/msg owner hello"));
assertTrue(JigsawStudioProtection.blocksMutatingCommand(ownerId, otherId, "/execute run say bypass"));
assertTrue(JigsawStudioProtection.blocksMutatingCommand(ownerId, otherId, "/unknownplugin mutate"));
assertTrue(JigsawStudioProtection.blocksNonEditableWorkcellMutation(
true, "/fill 0 0 0 1 1 1 stone"));
assertTrue(JigsawStudioService.blocksNonEditableWorkcellMutation(true, "//paste -a"));
assertFalse(JigsawStudioService.blocksNonEditableWorkcellMutation(
assertTrue(JigsawStudioProtection.blocksNonEditableWorkcellMutation(true, "//paste -a"));
assertFalse(JigsawStudioProtection.blocksNonEditableWorkcellMutation(
true, "/iris jigsaw variant load test/end"));
assertFalse(JigsawStudioService.blocksNonEditableWorkcellMutation(
assertFalse(JigsawStudioProtection.blocksNonEditableWorkcellMutation(
false, "/fill 0 0 0 1 1 1 stone"));
Method method = JigsawStudioService.class.getMethod(
Method method = JigsawStudioProtectionListener.class.getMethod(
"onUnauthorizedPlayerCommand",
PlayerCommandPreprocessEvent.class);
EventHandler handler = method.getAnnotation(EventHandler.class);
@@ -335,21 +340,21 @@ public class JigsawStudioServiceCaptureTest {
UUID ownerId = UUID.fromString("11111111-1111-1111-1111-111111111111");
UUID otherId = UUID.fromString("22222222-2222-2222-2222-222222222222");
assertFalse(JigsawStudioService.blocksStudioInventoryMutation(
assertFalse(JigsawStudioProtection.blocksStudioInventoryMutation(
false, false, true, true, ownerId, otherId));
assertFalse(JigsawStudioService.blocksStudioInventoryMutation(
assertFalse(JigsawStudioProtection.blocksStudioInventoryMutation(
true, false, false, false, ownerId, ownerId));
assertTrue(JigsawStudioService.blocksStudioInventoryMutation(
assertTrue(JigsawStudioProtection.blocksStudioInventoryMutation(
true, false, true, false, ownerId, ownerId));
assertTrue(JigsawStudioService.blocksStudioInventoryMutation(
assertTrue(JigsawStudioProtection.blocksStudioInventoryMutation(
true, false, false, true, ownerId, ownerId));
assertTrue(JigsawStudioService.blocksStudioInventoryMutation(
assertTrue(JigsawStudioProtection.blocksStudioInventoryMutation(
true, false, false, false, ownerId, otherId));
assertFalse(JigsawStudioService.blocksStudioInventoryMutation(
assertFalse(JigsawStudioProtection.blocksStudioInventoryMutation(
true, false, false, false, ownerId, null));
assertTrue(JigsawStudioService.blocksStudioInventoryMutation(
assertTrue(JigsawStudioProtection.blocksStudioInventoryMutation(
true, true, false, false, ownerId, null));
assertTrue(JigsawStudioService.blocksStudioInventoryMutation(
assertTrue(JigsawStudioProtection.blocksStudioInventoryMutation(
true, true, false, false, ownerId, ownerId));
}
@@ -389,17 +394,17 @@ public class JigsawStudioServiceCaptureTest {
KMap<String, Object> changedAgain = new KMap<>();
changedAgain.put("name", "iris:cap");
assertFalse(JigsawStudioService.tileSnapshotChanged(baseline, unchanged));
assertTrue(JigsawStudioService.tileSnapshotChanged(baseline, changed));
assertFalse(JigsawStudioTileWatcher.tileSnapshotChanged(baseline, unchanged));
assertTrue(JigsawStudioTileWatcher.tileSnapshotChanged(baseline, changed));
assertEquals(
new JigsawStudioService.JigsawTilePollDecision(true, true),
JigsawStudioService.jigsawTilePollDecision(baseline, changed, false));
new JigsawStudioTileWatcher.JigsawTilePollDecision(true, true),
JigsawStudioTileWatcher.jigsawTilePollDecision(baseline, changed, false));
assertEquals(
new JigsawStudioService.JigsawTilePollDecision(true, true),
JigsawStudioService.jigsawTilePollDecision(changed, changedAgain, false));
new JigsawStudioTileWatcher.JigsawTilePollDecision(true, true),
JigsawStudioTileWatcher.jigsawTilePollDecision(changed, changedAgain, false));
assertEquals(
new JigsawStudioService.JigsawTilePollDecision(false, false),
JigsawStudioService.jigsawTilePollDecision(changedAgain, changedAgain, true));
new JigsawStudioTileWatcher.JigsawTilePollDecision(false, false),
JigsawStudioTileWatcher.jigsawTilePollDecision(changedAgain, changedAgain, true));
}
@Test
@@ -407,14 +412,14 @@ public class JigsawStudioServiceCaptureTest {
UUID requestId = UUID.fromString("33333333-3333-3333-3333-333333333333");
IOException validationFailure = new IOException("closure rejected an empty piece entry");
String validationDetail = "Jigsaw Studio save failed: closure rejected an empty piece entry";
String validationContext = JigsawStudioService.autosaveFailureContext(
String validationContext = JigsawStudioAutosaveScheduler.autosaveFailureContext(
requestId,
"qa/profile_final",
"workcell/end",
"qa/profile_final/end",
validationDetail);
JigsawStudioService.AutosaveFailureState validationState =
new JigsawStudioService.AutosaveFailureState();
JigsawStudioAutosaveScheduler.AutosaveFailureState validationState =
new JigsawStudioAutosaveScheduler.AutosaveFailureState();
StructureWriteResult writerResult = new StructureWriteResult(
StructureWriteResult.Status.OWNERSHIP_CONFLICT,
StructureWriteResult.Action.NONE,
@@ -425,18 +430,18 @@ public class JigsawStudioServiceCaptureTest {
"",
Optional.empty());
String writerDetail = JigsawStudioService.writeFailure(writerResult);
String writerContext = JigsawStudioService.autosaveFailureContext(
String writerContext = JigsawStudioAutosaveScheduler.autosaveFailureContext(
requestId,
"qa/profile_final",
"workcell/end",
"qa/profile_final/end",
writerDetail);
JigsawStudioService.AutosaveFailureState writerState =
new JigsawStudioService.AutosaveFailureState();
JigsawStudioAutosaveScheduler.AutosaveFailureState writerState =
new JigsawStudioAutosaveScheduler.AutosaveFailureState();
try (MockedStatic<IrisLogging> logging = mockStatic(IrisLogging.class)) {
JigsawStudioService.AutosaveFailureDecision firstValidation =
JigsawStudioService.recordPersistentAutosaveFailure(
JigsawStudioAutosaveScheduler.AutosaveFailureDecision firstValidation =
JigsawStudioAutosaveScheduler.recordPersistentAutosaveFailure(
validationState,
requestId,
"qa/profile_final",
@@ -444,8 +449,8 @@ public class JigsawStudioServiceCaptureTest {
"qa/profile_final/end",
validationDetail,
validationFailure);
JigsawStudioService.AutosaveFailureDecision secondValidation =
JigsawStudioService.recordPersistentAutosaveFailure(
JigsawStudioAutosaveScheduler.AutosaveFailureDecision secondValidation =
JigsawStudioAutosaveScheduler.recordPersistentAutosaveFailure(
validationState,
requestId,
"qa/profile_final",
@@ -455,7 +460,7 @@ public class JigsawStudioServiceCaptureTest {
validationFailure);
List<Integer> writerDelays = new ArrayList<>();
for (int attempt = 0; attempt < 7; attempt++) {
writerDelays.add(JigsawStudioService.recordPersistentAutosaveFailure(
writerDelays.add(JigsawStudioAutosaveScheduler.recordPersistentAutosaveFailure(
writerState,
requestId,
"qa/profile_final",
@@ -476,8 +481,8 @@ public class JigsawStudioServiceCaptureTest {
logging.verify(() -> IrisLogging.warn("%s", writerContext), times(1));
}
JigsawStudioService.AutosaveFailureDecision newIdentity =
new JigsawStudioService.AutosaveFailureState().recordPersistentFailure();
JigsawStudioAutosaveScheduler.AutosaveFailureDecision newIdentity =
new JigsawStudioAutosaveScheduler.AutosaveFailureState().recordPersistentFailure();
assertEquals(40, newIdentity.retryTicks());
assertTrue(newIdentity.logFailure());
}
@@ -540,11 +545,11 @@ public class JigsawStudioServiceCaptureTest {
when(generator.getLayout()).thenAnswer(invocation -> session.layout());
World world = mock(World.class);
when(world.getUID()).thenReturn(worldId);
ConcurrentHashMap<String, JigsawStudioService.BayPopulation> populations = new ConcurrentHashMap<>();
JigsawStudioService.BayPopulation endPopulation =
new JigsawStudioService.BayPopulation(Set.of(0L), "");
JigsawStudioService.BayPopulation straightPopulation =
new JigsawStudioService.BayPopulation(Set.of(0L), "");
ConcurrentHashMap<String, JigsawStudioChunkWriter.BayPopulation> populations = new ConcurrentHashMap<>();
JigsawStudioChunkWriter.BayPopulation endPopulation =
new JigsawStudioChunkWriter.BayPopulation(Set.of(0L), "");
JigsawStudioChunkWriter.BayPopulation straightPopulation =
new JigsawStudioChunkWriter.BayPopulation(Set.of(0L), "");
endPopulation.markFullyReady();
straightPopulation.markFullyReady();
populations.put(initialEnd.stableId(), endPopulation);
@@ -567,7 +572,7 @@ public class JigsawStudioServiceCaptureTest {
populations,
ConcurrentHashMap.newKeySet(),
new AtomicLong());
Method scheduleAutosave = JigsawStudioService.class.getDeclaredMethod(
Method scheduleAutosave = JigsawStudioAutosaveScheduler.class.getDeclaredMethod(
"scheduleAutosave",
studioType,
JigsawStudioBay.class,
@@ -601,8 +606,8 @@ public class JigsawStudioServiceCaptureTest {
anyInt(),
any(Runnable.class)))
.thenReturn(true);
scheduleAutosave.invoke(service, studio, initialEnd, endDirty, 40);
scheduleAutosave.invoke(service, studio, initialStraight, straightDirty, 40);
scheduleAutosave.invoke(service.autosaveScheduler, studio, initialEnd, endDirty, 40);
scheduleAutosave.invoke(service.autosaveScheduler, studio, initialStraight, straightDirty, 40);
assertEquals(2, delayedAutosaves.size());
JigsawStudioSession.SaveIdentity committedEnd = session.beginSave(initialEnd.stableId())
@@ -614,7 +619,7 @@ public class JigsawStudioServiceCaptureTest {
dimensions,
catalog);
assertTrue(session.replaceLayout(replacementLayout));
JigsawStudioBay currentStraight = JigsawStudioService.resolveCurrentAutosaveBay(
JigsawStudioBay currentStraight = JigsawStudioAutosaveScheduler.resolveCurrentAutosaveBay(
session,
initialStraight.stableId());
assertNotSame(initialStraight, currentStraight);
@@ -671,10 +676,10 @@ public class JigsawStudioServiceCaptureTest {
when(player.getWorld()).thenReturn(world);
JigsawStudioService service = new JigsawStudioService();
ConcurrentHashMap<String, JigsawStudioService.BayPopulation> populations = new ConcurrentHashMap<>();
ConcurrentHashMap<String, JigsawStudioChunkWriter.BayPopulation> populations = new ConcurrentHashMap<>();
populations.put(
bay.stableId(),
new JigsawStudioService.BayPopulation(Set.of(0L), "workcell hydration failed"));
new JigsawStudioChunkWriter.BayPopulation(Set.of(0L), "workcell hydration failed"));
Class<?> studioType = Class.forName(JigsawStudioService.class.getName() + "$ActiveStudio");
Constructor<?> studioConstructor = studioType.getDeclaredConstructor(
UUID.class,
@@ -699,11 +704,11 @@ public class JigsawStudioServiceCaptureTest {
Map<UUID, Object> studios = (Map<UUID, Object>) studiosField.get(service);
studios.put(worldId, studio);
Class<?> keyType = Class.forName(JigsawStudioService.class.getName() + "$AutosaveKey");
Class<?> keyType = Class.forName(JigsawStudioAutosaveScheduler.class.getName() + "$AutosaveKey");
Constructor<?> keyConstructor = keyType.getDeclaredConstructor(UUID.class, String.class);
keyConstructor.setAccessible(true);
Object key = keyConstructor.newInstance(requestId, bay.stableId());
Class<?> ticketType = Class.forName(JigsawStudioService.class.getName() + "$AutosaveTicket");
Class<?> ticketType = Class.forName(JigsawStudioAutosaveScheduler.class.getName() + "$AutosaveTicket");
Constructor<?> ticketConstructor = ticketType.getDeclaredConstructor(
keyType,
studioType,
@@ -717,15 +722,15 @@ public class JigsawStudioServiceCaptureTest {
identity,
new AtomicBoolean(false),
new AtomicBoolean(false));
Field autosavesField = JigsawStudioService.class.getDeclaredField("autosaves");
Field autosavesField = JigsawStudioAutosaveScheduler.class.getDeclaredField("autosaves");
autosavesField.setAccessible(true);
@SuppressWarnings("unchecked")
Map<Object, Object> autosaves = (Map<Object, Object>) autosavesField.get(service);
Map<Object, Object> autosaves = (Map<Object, Object>) autosavesField.get(service.autosaveScheduler);
autosaves.put(key, ticket);
Method failureStateAccessor = ticketType.getDeclaredMethod("failureState");
failureStateAccessor.setAccessible(true);
JigsawStudioService.AutosaveFailureState failureState =
(JigsawStudioService.AutosaveFailureState) failureStateAccessor.invoke(ticket);
JigsawStudioAutosaveScheduler.AutosaveFailureState failureState =
(JigsawStudioAutosaveScheduler.AutosaveFailureState) failureStateAccessor.invoke(ticket);
assertEquals(40, failureState.recordPersistentFailure().retryTicks());
try (MockedStatic<J> scheduling = mockStatic(J.class)) {
@@ -740,13 +745,13 @@ public class JigsawStudioServiceCaptureTest {
populations.put(
bay.stableId(),
new JigsawStudioService.BayPopulation(Set.of(0L), ""));
new JigsawStudioChunkWriter.BayPopulation(Set.of(0L), ""));
assertFalse(service.flushAutosave(player, bay.stableId()));
assertEquals(1, autosaves.size());
assertTrue(session.isDirtyCurrent(identity));
JigsawStudioService.BayPopulation ready =
new JigsawStudioService.BayPopulation(Set.of(0L), "");
JigsawStudioChunkWriter.BayPopulation ready =
new JigsawStudioChunkWriter.BayPopulation(Set.of(0L), "");
ready.markFullyReady();
populations.put(bay.stableId(), ready);
JigsawStudioSession.SaveIdentity inProgress = session.beginSave(bay.stableId())
@@ -768,13 +773,13 @@ public class JigsawStudioServiceCaptureTest {
JigsawStudioVariant fixed = planarVariant(false, true);
JigsawStudioVariant readOnly = planarVariant(true, false);
assertFalse(JigsawStudioService.canToggleVariantRotation(
assertFalse(JigsawStudioProtection.canToggleVariantRotation(
JigsawStudioCompatibilityTarget.VANILLA_PORTABLE, rotatable));
assertTrue(JigsawStudioService.canToggleVariantRotation(
assertTrue(JigsawStudioProtection.canToggleVariantRotation(
JigsawStudioCompatibilityTarget.VANILLA_PORTABLE, fixed));
assertTrue(JigsawStudioService.canToggleVariantRotation(
assertTrue(JigsawStudioProtection.canToggleVariantRotation(
JigsawStudioCompatibilityTarget.IRIS_EXTENDED, rotatable));
assertFalse(JigsawStudioService.canToggleVariantRotation(
assertFalse(JigsawStudioProtection.canToggleVariantRotation(
JigsawStudioCompatibilityTarget.IRIS_EXTENDED, readOnly));
}
@@ -788,22 +793,22 @@ public class JigsawStudioServiceCaptureTest {
IrisJigsawConnector eastSource = connector().setDirection(IrisDirection.EAST_POSITIVE_X);
IrisJigsawConnector southDisplayed = connector().setDirection(IrisDirection.SOUTH_POSITIVE_Z);
JigsawStudioService.requireWorkcellTopology(
JigsawStudioCapture.requireWorkcellTopology(
layout.get("workcell/end"),
List.of(eastSource),
3);
JigsawStudioService.WorkcellTopologyException wrongDirection = assertThrows(
JigsawStudioService.WorkcellTopologyException.class,
() -> JigsawStudioService.requireWorkcellTopology(
JigsawStudioCapture.WorkcellTopologyException wrongDirection = assertThrows(
JigsawStudioCapture.WorkcellTopologyException.class,
() -> JigsawStudioCapture.requireWorkcellTopology(
layout.get("workcell/end"),
List.of(southDisplayed),
0));
assertTrue(wrongDirection.getMessage().contains("south end (1 horizontal connector)"));
assertTrue(wrongDirection.getMessage().contains("Reset Connector Blocks"));
JigsawStudioService.WorkcellTopologyException missingTee = assertThrows(
JigsawStudioService.WorkcellTopologyException.class,
() -> JigsawStudioService.requireWorkcellTopology(
JigsawStudioCapture.WorkcellTopologyException missingTee = assertThrows(
JigsawStudioCapture.WorkcellTopologyException.class,
() -> JigsawStudioCapture.requireWorkcellTopology(
layout.get("workcell/tee"),
List.of(),
0));
@@ -818,13 +823,13 @@ public class JigsawStudioServiceCaptureTest {
64,
0,
new JigsawStudioCellDimensions(3, 1, 3));
List<JigsawStudioService.ChunkCaptureArea> areas = JigsawStudioService.chunkIntersections(bounds);
List<JigsawStudioCapture.ChunkCaptureArea> areas = JigsawStudioCapture.chunkIntersections(bounds);
PlatformBlockState sourceState = BukkitBlockState.of(directionalBlockData(BlockFace.NORTH));
for (int quarterTurns = 1; quarterTurns <= 3; quarterTurns++) {
IrisObjectRotation displayRotation = IrisObjectRotation.of(0, -90.0D * quarterTurns, 0);
PlatformBlockState displayedState = displayRotation.rotate(sourceState, 0, 0, 0);
IrisDirection displayedDirection = displayRotation.rotate(IrisDirection.NORTH_NEGATIVE_Z);
JigsawStudioService.CapturedConnector connector = new JigsawStudioService.CapturedConnector(
JigsawStudioCapture.CapturedConnector connector = new JigsawStudioCapture.CapturedConnector(
1,
0,
1,
@@ -838,14 +843,14 @@ public class JigsawStudioServiceCaptureTest {
displayedState.key(),
-3,
7);
JigsawStudioService.ChunkSnapshot snapshot = new JigsawStudioService.ChunkSnapshot(
JigsawStudioCapture.ChunkSnapshot snapshot = new JigsawStudioCapture.ChunkSnapshot(
areas.getFirst(),
List.of(),
List.of(connector));
try (MockedStatic<B> blocks = mockStatic(B.class)) {
blocks.when(() -> B.getStateOrNull(
displayedState.key(), false)).thenReturn(displayedState);
JigsawStudioService.Capture capture = JigsawStudioService.aggregateSnapshots(
JigsawStudioCapture.Capture capture = JigsawStudioCapture.aggregateSnapshots(
bounds,
areas,
List.of(snapshot),
@@ -883,12 +888,12 @@ public class JigsawStudioServiceCaptureTest {
when(block.getBlockData()).thenReturn(chestData);
World world = mock(World.class);
when(world.getBlockAt(0, 64, 0)).thenReturn(block);
JigsawStudioService.ChunkCaptureArea area = JigsawStudioService.chunkIntersections(bounds).getFirst();
JigsawStudioCapture.ChunkCaptureArea area = JigsawStudioCapture.chunkIntersections(bounds).getFirst();
JigsawStudioService.ChunkSnapshot snapshot;
JigsawStudioCapture.ChunkSnapshot snapshot;
try (MockedStatic<TileData> tiles = mockStatic(TileData.class)) {
tiles.when(() -> TileData.getTileState(block, false)).thenReturn(tileData);
snapshot = JigsawStudioService.captureChunkIntersection(
snapshot = JigsawStudioCapture.captureChunkIntersection(
world,
bounds,
piece,
@@ -897,7 +902,7 @@ public class JigsawStudioServiceCaptureTest {
0,
false);
}
JigsawStudioService.Capture capture = JigsawStudioService.aggregateSnapshots(
JigsawStudioCapture.Capture capture = JigsawStudioCapture.aggregateSnapshots(
bounds,
List.of(area),
List.of(snapshot));
@@ -940,7 +945,7 @@ public class JigsawStudioServiceCaptureTest {
IrisJigsawPiece source = pieceWithPlanarConnectors(dimensions, authoredOrders.get(index));
List<IrisJigsawConnector> captured = planarConnectors(dimensions, captureOrders.get(index));
List<IrisJigsawConnector> ordered = JigsawStudioService.preserveCapturedConnectorOrder(
List<IrisJigsawConnector> ordered = JigsawStudioCapture.preserveCapturedConnectorOrder(
source,
captured);
@@ -969,7 +974,7 @@ public class JigsawStudioServiceCaptureTest {
dimensions,
IrisDirection.EAST_POSITIVE_X);
List<IrisJigsawConnector> ordered = JigsawStudioService.preserveCapturedConnectorOrder(
List<IrisJigsawConnector> ordered = JigsawStudioCapture.preserveCapturedConnectorOrder(
source,
List.of(capturedEast, capturedNorth));
@@ -993,7 +998,7 @@ public class JigsawStudioServiceCaptureTest {
dimensions,
IrisDirection.NORTH_NEGATIVE_Z);
List<IrisJigsawConnector> ordered = JigsawStudioService.preserveCapturedConnectorOrder(
List<IrisJigsawConnector> ordered = JigsawStudioCapture.preserveCapturedConnectorOrder(
source,
List.of(capturedWest, capturedNorth));
@@ -1015,7 +1020,7 @@ public class JigsawStudioServiceCaptureTest {
IrisJigsawConnector highZ = connectorAt(2, 3, 10);
IrisJigsawConnector lowZ = connectorAt(2, 3, 4);
List<IrisJigsawConnector> ordered = JigsawStudioService.preserveCapturedConnectorOrder(
List<IrisJigsawConnector> ordered = JigsawStudioCapture.preserveCapturedConnectorOrder(
source,
List.of(highY, capturedNorth, highZ, lowZ));
@@ -1035,10 +1040,10 @@ public class JigsawStudioServiceCaptureTest {
duplicateSource.getConnectors().add(second);
IrisJigsawPiece emptySource = new IrisJigsawPiece().setConnectors(new KList<>());
assertThrows(IOException.class, () -> JigsawStudioService.preserveCapturedConnectorOrder(
assertThrows(IOException.class, () -> JigsawStudioCapture.preserveCapturedConnectorOrder(
duplicateSource,
List.of(first)));
assertThrows(IOException.class, () -> JigsawStudioService.preserveCapturedConnectorOrder(
assertThrows(IOException.class, () -> JigsawStudioCapture.preserveCapturedConnectorOrder(
emptySource,
List.of(first, second)));
}
@@ -1050,7 +1055,7 @@ public class JigsawStudioServiceCaptureTest {
64,
0,
new JigsawStudioCellDimensions(5, 1, 3));
List<JigsawStudioService.ChunkCaptureArea> areas = JigsawStudioService.chunkIntersections(bounds);
List<JigsawStudioCapture.ChunkCaptureArea> areas = JigsawStudioCapture.chunkIntersections(bounds);
IrisJigsawConnector sourceFirst = connectorAt(0, 0, 1)
.setDirection(IrisDirection.NORTH_NEGATIVE_Z);
IrisJigsawConnector sourceSecond = connectorAt(2, 0, 2)
@@ -1064,31 +1069,31 @@ public class JigsawStudioServiceCaptureTest {
for (int quarterTurns = 1; quarterTurns <= 3; quarterTurns++) {
IrisObjectRotation displayRotation = IrisObjectRotation.of(0, -90.0D * quarterTurns, 0);
PlatformBlockState displayedState = displayRotation.rotate(sourceState, 0, 0, 0);
JigsawStudioService.CapturedConnector displayedFirst = displayedConnector(
JigsawStudioCapture.CapturedConnector displayedFirst = displayedConnector(
sourceFirst,
bounds.dimensions(),
quarterTurns,
displayRotation,
displayedState.key());
JigsawStudioService.CapturedConnector displayedSecond = displayedConnector(
JigsawStudioCapture.CapturedConnector displayedSecond = displayedConnector(
sourceSecond,
bounds.dimensions(),
quarterTurns,
displayRotation,
displayedState.key());
JigsawStudioService.ChunkSnapshot snapshot = new JigsawStudioService.ChunkSnapshot(
JigsawStudioCapture.ChunkSnapshot snapshot = new JigsawStudioCapture.ChunkSnapshot(
areas.getFirst(),
List.of(),
List.of(displayedSecond, displayedFirst));
try (MockedStatic<B> blocks = mockStatic(B.class)) {
blocks.when(() -> B.getStateOrNull(displayedState.key(), false)).thenReturn(displayedState);
JigsawStudioService.Capture capture = JigsawStudioService.aggregateSnapshots(
JigsawStudioCapture.Capture capture = JigsawStudioCapture.aggregateSnapshots(
bounds,
areas,
List.of(snapshot),
quarterTurns);
List<IrisJigsawConnector> ordered = JigsawStudioService.preserveCapturedConnectorOrder(
List<IrisJigsawConnector> ordered = JigsawStudioCapture.preserveCapturedConnectorOrder(
source,
capture.connectors());
@@ -1105,7 +1110,7 @@ public class JigsawStudioServiceCaptureTest {
64,
0,
new JigsawStudioCellDimensions(5, 2, 3));
List<JigsawStudioService.ChunkCaptureArea> areas = JigsawStudioService.chunkIntersections(bounds);
List<JigsawStudioCapture.ChunkCaptureArea> areas = JigsawStudioCapture.chunkIntersections(bounds);
KMap<String, Object> properties = new KMap<>();
properties.put("CustomName", "QA Chest");
properties.put("Lock", "iris:test");
@@ -1119,18 +1124,18 @@ public class JigsawStudioServiceCaptureTest {
for (int quarterTurns = 1; quarterTurns <= 3; quarterTurns++) {
BlockData displayedData = directionalBlockData(Material.CHEST, BlockFace.EAST);
PlatformBlockState displayedState = BukkitBlockState.of(displayedData);
JigsawStudioService.CapturedBlock capturedBlock = new JigsawStudioService.CapturedBlock(
JigsawStudioCapture.CapturedBlock capturedBlock = new JigsawStudioCapture.CapturedBlock(
1,
1,
1,
displayedState,
tileData);
JigsawStudioService.ChunkSnapshot snapshot = new JigsawStudioService.ChunkSnapshot(
JigsawStudioCapture.ChunkSnapshot snapshot = new JigsawStudioCapture.ChunkSnapshot(
areas.getFirst(),
List.of(capturedBlock),
List.of());
JigsawStudioService.Capture capture = JigsawStudioService.aggregateSnapshots(
JigsawStudioCapture.Capture capture = JigsawStudioCapture.aggregateSnapshots(
bounds,
areas,
List.of(snapshot),
@@ -1158,7 +1163,7 @@ public class JigsawStudioServiceCaptureTest {
List.of(new JigsawStudioGenerator.RenderedBlock(0, 0, 0, state, tileData)),
List.of());
assertEquals("", JigsawStudioService.validateMaterialization(rendered));
assertEquals("", JigsawStudioChunkWriter.validateMaterialization(rendered));
}
@Test
@@ -1167,8 +1172,8 @@ public class JigsawStudioServiceCaptureTest {
BlockData air = blockData(Material.AIR, "minecraft:air");
BlockData structureVoid = blockData(Material.STRUCTURE_VOID, "minecraft:structure_void");
JigsawStudioService.storeConnectorFinalState(object, 0, 0, 0, air);
JigsawStudioService.storeConnectorFinalState(object, 1, 0, 0, structureVoid);
JigsawStudioCapture.storeConnectorFinalState(object, 0, 0, 0, air);
JigsawStudioCapture.storeConnectorFinalState(object, 1, 0, 0, structureVoid);
IrisBlockVector airPosition = object.getSigned(0, 0, 0);
IrisBlockVector structureVoidPosition = object.getSigned(1, 0, 0);
@@ -1189,10 +1194,10 @@ public class JigsawStudioServiceCaptureTest {
when(sourceAir.key()).thenReturn("minecraft:air");
source.setUnsigned(0, 0, 0, sourceAir);
PlatformBlockState retained = JigsawStudioService.retainedSourceAir(source, 0, 0, 0, air);
PlatformBlockState retained = JigsawStudioCapture.retainedSourceAir(source, 0, 0, 0, air);
assertNotNull(retained);
assertNull(JigsawStudioService.retainedSourceAir(source, 1, 0, 0, air));
assertNull(JigsawStudioService.retainedSourceAir(source, 0, 0, 0, structureVoid));
assertNull(JigsawStudioCapture.retainedSourceAir(source, 1, 0, 0, air));
assertNull(JigsawStudioCapture.retainedSourceAir(source, 0, 0, 0, structureVoid));
IrisObject captured = new IrisObject(2, 2, 2);
captured.setUnsigned(0, 0, 0, retained);
@@ -1234,7 +1239,7 @@ public class JigsawStudioServiceCaptureTest {
IrisJigsawPiece piece = new IrisJigsawPiece().setConnectors(new KList<>());
piece.getConnectors().add(source);
KMap<String, Object> nbt = JigsawStudioService.markerNbt(source);
KMap<String, Object> nbt = JigsawStudioChunkWriter.markerNbt(source);
assertEquals("iris:fort/start", nbt.get("pool"));
assertEquals("iris:door", nbt.get("name"));
assertEquals(-4, nbt.get("selection_priority"));
@@ -1245,7 +1250,7 @@ public class JigsawStudioServiceCaptureTest {
.setPool("fort/start")
.setName("iris:door")
.setTargetName("iris:door");
JigsawStudioService.restoreCapturedMetadata(captured, piece);
JigsawStudioChunkWriter.restoreCapturedMetadata(captured, piece);
assertEquals("fort/start", captured.getPool());
assertEquals("door", captured.getName());
@@ -1255,7 +1260,7 @@ public class JigsawStudioServiceCaptureTest {
@Test
public void authoredBayReadinessRequiresEveryChunkToPopulateAndHydrate() {
JigsawStudioService.BayPopulation population = new JigsawStudioService.BayPopulation(
JigsawStudioChunkWriter.BayPopulation population = new JigsawStudioChunkWriter.BayPopulation(
Set.of(11L, 12L), "");
assertFalse(population.readiness().ready());
@@ -1270,7 +1275,7 @@ public class JigsawStudioServiceCaptureTest {
population.markApplied(12L);
population.markHydrated(12L);
JigsawStudioService.BayReadiness readiness = population.readiness();
JigsawStudioSaveLifecycle.BayReadiness readiness = population.readiness();
assertTrue(readiness.ready());
assertEquals(2, readiness.requiredChunks());
assertEquals(2, readiness.generatedChunks());
@@ -1288,9 +1293,9 @@ public class JigsawStudioServiceCaptureTest {
new JigsawStudioVariantCatalog(List.of(planarVariant(true, true))));
JigsawStudioBay blank = layout.get("workcell/blank");
long chunkKey = ((long) 1 << 32) ^ 1L;
JigsawStudioService.BayPopulation population =
new JigsawStudioService.BayPopulation(Set.of(chunkKey), "");
ConcurrentHashMap<String, JigsawStudioService.BayPopulation> populations = new ConcurrentHashMap<>();
JigsawStudioChunkWriter.BayPopulation population =
new JigsawStudioChunkWriter.BayPopulation(Set.of(chunkKey), "");
ConcurrentHashMap<String, JigsawStudioChunkWriter.BayPopulation> populations = new ConcurrentHashMap<>();
populations.put(blank.stableId(), population);
World world = mock(World.class);
@@ -1334,7 +1339,7 @@ public class JigsawStudioServiceCaptureTest {
when(event.getWorld()).thenReturn(world);
when(event.getChunk()).thenReturn(chunk);
service.onChunkLoad(event);
service.protectionListener.onChunkLoad(event);
assertTrue(population.needsApplication(chunkKey));
assertEquals(1, population.readiness().generatedChunks());
@@ -1343,7 +1348,7 @@ public class JigsawStudioServiceCaptureTest {
@Test
public void hydrationFailurePermanentlyBlocksSaveReadiness() {
JigsawStudioService.BayPopulation population = new JigsawStudioService.BayPopulation(
JigsawStudioChunkWriter.BayPopulation population = new JigsawStudioChunkWriter.BayPopulation(
Set.of(27L), "");
population.markGenerated(27L);
population.markApplied(27L);
@@ -1364,20 +1369,20 @@ public class JigsawStudioServiceCaptureTest {
9,
new JigsawStudioCellDimensions(25, 3, 20));
List<JigsawStudioService.ChunkCaptureArea> areas = JigsawStudioService.chunkIntersections(bounds);
List<JigsawStudioCapture.ChunkCaptureArea> areas = JigsawStudioCapture.chunkIntersections(bounds);
assertEquals(List.of(
new JigsawStudioService.ChunkCaptureArea(-1, 0, 0, 5, 0, 7),
new JigsawStudioService.ChunkCaptureArea(-1, 1, 0, 5, 7, 20),
new JigsawStudioService.ChunkCaptureArea(0, 0, 5, 21, 0, 7),
new JigsawStudioService.ChunkCaptureArea(0, 1, 5, 21, 7, 20),
new JigsawStudioService.ChunkCaptureArea(1, 0, 21, 25, 0, 7),
new JigsawStudioService.ChunkCaptureArea(1, 1, 21, 25, 7, 20)
new JigsawStudioCapture.ChunkCaptureArea(-1, 0, 0, 5, 0, 7),
new JigsawStudioCapture.ChunkCaptureArea(-1, 1, 0, 5, 7, 20),
new JigsawStudioCapture.ChunkCaptureArea(0, 0, 5, 21, 0, 7),
new JigsawStudioCapture.ChunkCaptureArea(0, 1, 5, 21, 7, 20),
new JigsawStudioCapture.ChunkCaptureArea(1, 0, 21, 25, 0, 7),
new JigsawStudioCapture.ChunkCaptureArea(1, 1, 21, 25, 7, 20)
), areas);
for (int x = 0; x < bounds.dimensions().width(); x++) {
for (int z = 0; z < bounds.dimensions().depth(); z++) {
int matches = 0;
for (JigsawStudioService.ChunkCaptureArea area : areas) {
for (JigsawStudioCapture.ChunkCaptureArea area : areas) {
if (area.contains(x, z)) {
matches++;
}
@@ -1394,28 +1399,28 @@ public class JigsawStudioServiceCaptureTest {
64,
0,
new JigsawStudioCellDimensions(20, 2, 1));
List<JigsawStudioService.ChunkCaptureArea> areas = JigsawStudioService.chunkIntersections(bounds);
List<JigsawStudioCapture.ChunkCaptureArea> areas = JigsawStudioCapture.chunkIntersections(bounds);
PlatformBlockState stone = mock(PlatformBlockState.class);
when(stone.key()).thenReturn("minecraft:stone");
JigsawStudioService.ChunkSnapshot first = new JigsawStudioService.ChunkSnapshot(
JigsawStudioCapture.ChunkSnapshot first = new JigsawStudioCapture.ChunkSnapshot(
areas.getFirst(),
List.of(new JigsawStudioService.CapturedBlock(0, 0, 0, stone, null)),
List.of(new JigsawStudioCapture.CapturedBlock(0, 0, 0, stone, null)),
List.of());
JigsawStudioService.ChunkSnapshot second = new JigsawStudioService.ChunkSnapshot(
JigsawStudioCapture.ChunkSnapshot second = new JigsawStudioCapture.ChunkSnapshot(
areas.getLast(),
List.of(new JigsawStudioService.CapturedBlock(8, 1, 0, stone, null)),
List.of(new JigsawStudioCapture.CapturedBlock(8, 1, 0, stone, null)),
List.of());
JigsawStudioService.Capture forward = JigsawStudioService.aggregateSnapshots(
JigsawStudioCapture.Capture forward = JigsawStudioCapture.aggregateSnapshots(
bounds, areas, List.of(first, second));
JigsawStudioService.Capture reverse = JigsawStudioService.aggregateSnapshots(
JigsawStudioCapture.Capture reverse = JigsawStudioCapture.aggregateSnapshots(
bounds, areas, List.of(second, first));
assertArrayEquals(forward.objectContent(), reverse.objectContent());
assertFalse(forward.hasBlockEntities());
assertTrue(forward.connectors().isEmpty());
assertThrows(IOException.class,
() -> JigsawStudioService.aggregateSnapshots(bounds, areas, List.of(first)));
() -> JigsawStudioCapture.aggregateSnapshots(bounds, areas, List.of(first)));
}
@Test
@@ -1424,14 +1429,14 @@ public class JigsawStudioServiceCaptureTest {
UUID firstRequest = UUID.fromString("44444444-4444-4444-4444-444444444444");
UUID secondRequest = UUID.fromString("55555555-5555-5555-5555-555555555555");
assertEquals(JigsawStudioService.SaveStart.STARTED, service.tryBeginSave(firstRequest));
assertEquals(JigsawStudioService.SaveStart.IN_PROGRESS, service.tryBeginSave(firstRequest));
assertEquals(JigsawStudioService.SaveStart.STARTED, service.tryBeginSave(secondRequest));
assertEquals(JigsawStudioSaveLifecycle.SaveStart.STARTED, service.saveLifecycle.tryBeginSave(firstRequest));
assertEquals(JigsawStudioSaveLifecycle.SaveStart.IN_PROGRESS, service.saveLifecycle.tryBeginSave(firstRequest));
assertEquals(JigsawStudioSaveLifecycle.SaveStart.STARTED, service.saveLifecycle.tryBeginSave(secondRequest));
service.finishSave(firstRequest);
assertEquals(JigsawStudioService.SaveStart.STARTED, service.tryBeginSave(firstRequest));
service.finishSave(firstRequest);
service.finishSave(secondRequest);
service.saveLifecycle.finishSave(firstRequest);
assertEquals(JigsawStudioSaveLifecycle.SaveStart.STARTED, service.saveLifecycle.tryBeginSave(firstRequest));
service.saveLifecycle.finishSave(firstRequest);
service.saveLifecycle.finishSave(secondRequest);
}
private static IrisJigsawConnector connector() {
@@ -1491,7 +1496,7 @@ public class JigsawStudioServiceCaptureTest {
return directions;
}
private static JigsawStudioService.CapturedConnector displayedConnector(
private static JigsawStudioCapture.CapturedConnector displayedConnector(
IrisJigsawConnector source,
JigsawStudioCellDimensions displayDimensions,
int displayRotationQuarterTurns,
@@ -1502,7 +1507,7 @@ public class JigsawStudioServiceCaptureTest {
source.getPosition(),
displayDimensions,
displayRotationQuarterTurns);
return new JigsawStudioService.CapturedConnector(
return new JigsawStudioCapture.CapturedConnector(
displayedPosition.getX(),
displayedPosition.getY(),
displayedPosition.getZ(),
@@ -1624,7 +1629,7 @@ public class JigsawStudioServiceCaptureTest {
EventPriority priority,
boolean ignoreCancelled
) throws NoSuchMethodException {
Method method = JigsawStudioService.class.getMethod(methodName, eventType);
Method method = JigsawStudioProtectionListener.class.getMethod(methodName, eventType);
EventHandler annotation = method.getAnnotation(EventHandler.class);
assertNotNull(annotation);
assertEquals(priority, annotation.priority());
@@ -81,10 +81,10 @@ public class JigsawStudioVisualizationBoundsTest {
List<Emission> preview = emissions(fixture.player());
clearInvocations(fixture.player());
Method draw = JigsawStudioService.class.getDeclaredMethod("drawBounds", Player.class, Location.class,
JigsawStudioBounds.class, Color.class, float.class, double.class, nested("ParticleBudget"));
Method draw = JigsawStudioVisualization.class.getDeclaredMethod("drawBounds", Player.class, Location.class,
JigsawStudioBounds.class, Color.class, float.class, double.class, visualizationNested("ParticleBudget"));
draw.setAccessible(true);
Field color = JigsawStudioService.class.getDeclaredField("ASSEMBLY_PREVIEW_COLOR");
Field color = JigsawStudioVisualization.class.getDeclaredField("ASSEMBLY_PREVIEW_COLOR");
color.setAccessible(true);
invoke(draw, null, fixture.player(), viewer, bounds, color.get(null), 0.85F, 2.0D, budget(384));
@@ -122,10 +122,10 @@ public class JigsawStudioVisualizationBoundsTest {
assertTrue(fixture.service().showAssemblyPreview(fixture.player(), List.of(piece)));
}
Method draw = JigsawStudioService.class.getDeclaredMethod("drawAssemblyPreview", Player.class,
Location.class, nested("ParticleBudget"));
Method draw = JigsawStudioVisualization.class.getDeclaredMethod("drawAssemblyPreview", Player.class,
Location.class, visualizationNested("ParticleBudget"));
draw.setAccessible(true);
invoke(draw, fixture.service(), fixture.player(), new Location(fixture.world(), 201, 65, 201), budget(384));
invoke(draw, fixture.service().visualization, fixture.player(), new Location(fixture.world(), 201, 65, 201), budget(384));
assertTrue(emissions(fixture.player()).stream().anyMatch(emission ->
emission.x() == 201 && emission.y() == 65 && emission.z() == 201));
@@ -157,24 +157,24 @@ public class JigsawStudioVisualizationBoundsTest {
Object studio = constructor.newInstance(worldId, world, mock(Engine.class), generator,
new ConcurrentHashMap<>(), ConcurrentHashMap.newKeySet(), new AtomicLong());
JigsawStudioService service = new JigsawStudioService();
map(service, "studios").put(worldId, studio);
map(service, JigsawStudioService.class, "studios").put(worldId, studio);
return new Fixture(service, player, world, requestId, studio);
}
private static void evaluate(Fixture fixture, JigsawStudioPreviewRenderer.PreviewBounds bounds) throws Exception {
map(fixture.service(), "evaluations").put(fixture.requestId(), new JigsawStudioGraphEvaluation(
map(fixture.service().evaluator, JigsawStudioEvaluator.class, "evaluations").put(fixture.requestId(), new JigsawStudioGraphEvaluation(
fixture.requestId(), 1, 1337, JigsawStudioEvaluationState.VALID, "", 2, "", bounds));
}
private static void drawLive(Fixture fixture, Location viewer, int remaining) throws Exception {
Method draw = JigsawStudioService.class.getDeclaredMethod("drawLivePreview", Player.class, Location.class,
nested("ActiveStudio"), nested("ParticleBudget"));
Method draw = JigsawStudioVisualization.class.getDeclaredMethod("drawLivePreview", Player.class, Location.class,
nested("ActiveStudio"), visualizationNested("ParticleBudget"));
draw.setAccessible(true);
invoke(draw, fixture.service(), fixture.player(), viewer, fixture.studio(), budget(remaining));
invoke(draw, fixture.service().visualization, fixture.player(), viewer, fixture.studio(), budget(remaining));
}
private static Object budget(int remaining) throws Exception {
Constructor<?> constructor = nested("ParticleBudget").getDeclaredConstructor(int.class);
Constructor<?> constructor = visualizationNested("ParticleBudget").getDeclaredConstructor(int.class);
constructor.setAccessible(true);
return constructor.newInstance(remaining);
}
@@ -184,10 +184,14 @@ public class JigsawStudioVisualizationBoundsTest {
}
@SuppressWarnings("unchecked")
private static Map<UUID, Object> map(JigsawStudioService service, String name) throws Exception {
Field field = JigsawStudioService.class.getDeclaredField(name);
private static Map<UUID, Object> map(Object target, Class<?> owner, String name) throws Exception {
Field field = owner.getDeclaredField(name);
field.setAccessible(true);
return (Map<UUID, Object>) field.get(service);
return (Map<UUID, Object>) field.get(target);
}
private static Class<?> visualizationNested(String name) throws ClassNotFoundException {
return Class.forName(JigsawStudioVisualization.class.getName() + "$" + name);
}
private static void invoke(Method method, Object target, Object... arguments) throws Exception {
@@ -4,6 +4,7 @@ import art.arcane.iris.core.link.MultiverseCoreLink;
import art.arcane.iris.core.link.MultiverseGuardListener;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.testsupport.PlatformLeakGuard;
import art.arcane.iris.util.common.plugin.VolmitPlugin;
import org.bukkit.Bukkit;
import org.bukkit.event.Listener;
@@ -11,6 +12,7 @@ import org.bukkit.event.server.PluginDisableEvent;
import org.bukkit.event.server.PluginEnableEvent;
import org.bukkit.plugin.Plugin;
import org.junit.After;
import org.junit.ClassRule;
import org.junit.Test;
import org.mockito.MockedStatic;
@@ -24,6 +26,9 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class MultiverseSVCTest {
@ClassRule
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
@After
public void unbindLink() {
IrisServices.remove(MultiverseCoreLink.class);
@@ -7,8 +7,6 @@ import art.arcane.volmlib.util.format.Form;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.junit.Assert.assertEquals;
@@ -182,24 +180,6 @@ public class PackDownloadProgressReporterTest {
verify(sender, times(1)).sendMessage(anyString());
}
@Test
public void playerHudAlwaysShowsLoaderLaneAndActionBarWithCleanup() throws Exception {
String source = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/service/PackDownloadProgressReporter.java"
)).replace("\r\n", "\n");
assertFalse(source.contains("HudSlotRequest"));
assertFalse(source.contains("HudSurface"));
assertTrue(source.contains("J.ar(this::pulseHud, HUD_PULSE_TICKS)"));
assertTrue(source.contains("BukkitPlatform.hudLanes().show("));
assertTrue(source.contains("sender.sendAction(snapshot.line())"));
assertTrue(source.contains("HUD_TERMINAL_TICKS, retiredCleanup"));
assertTrue(source.contains("BukkitPlatform.hudLanes().retire(playerId, hudLaneId)"));
assertTrue(source.contains("BukkitPlatform.hudLanes().hide(player, hudLaneId)"));
assertFalse(source.contains("J.runGlobal(cleanup)"));
assertTrue(source.contains("J.car(activeTaskId);"));
}
@Test
public void allDownloadPhasesHaveLocalizedLabels() {
for (PackDownloader.DownloadPhase phase : PackDownloader.DownloadPhase.values()) {
@@ -5,76 +5,9 @@ import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.core.localization.PackDownloadMessages;
import org.junit.Test;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class StudioSVCPackDownloadContractTest {
@Test
public void downloadsUseReporterCompletionWithoutMutatingLiveDatapacks() throws Exception {
String source = Files.readString(Path.of("src/main/java/art/arcane/iris/core/service/StudioSVC.java")).replace("\r\n", "\n");
assertTrue(source.contains("reporter.succeed(result);"));
assertFalse(method(source, "public void downloadBuiltIn(VolmitSender sender, String key)")
.contains("ServerConfigurator.restart()"));
assertFalse(method(source, "public void downloadUrl(VolmitSender sender, String url)")
.contains("installDataPacksIfChanged"));
}
@Test
public void downloadLeaseIsAcquiredBeforeUnconditionalIoDispatch() throws Exception {
String source = Files.readString(Path.of("src/main/java/art/arcane/iris/core/service/StudioSVC.java")).replace("\r\n", "\n");
int mutationStart = source.indexOf("private void runPackMutation(");
int mutationEnd = source.indexOf("private void executePackMutation(", mutationStart);
String runPackMutation = source.substring(mutationStart, mutationEnd);
int leaseAcquisition = runPackMutation.indexOf("LifecycleOperationCoordinator.get().acquire(");
int executionTracking = runPackMutation.indexOf("new PackDownloadExecution(");
int ioDispatch = runPackMutation.indexOf("MultiBurst.ioBurst.submit(");
assertTrue(leaseAcquisition >= 0);
assertTrue(executionTracking > leaseAcquisition);
assertTrue(ioDispatch > leaseAcquisition);
assertTrue(runPackMutation.contains("execution.bind(future);"));
assertTrue(runPackMutation.contains("execution.cancel();"));
assertTrue(runPackMutation.contains("finally"));
assertTrue(runPackMutation.contains("reporter.start();"));
assertTrue(runPackMutation.contains("reporter.executionComplete();"));
assertFalse(runPackMutation.contains("runOffPrimaryThread"));
}
@Test
public void acceptedDownloadsRouteFeedbackProgressAndTerminalStatesThroughReporter() throws Exception {
String source = Files.readString(Path.of("src/main/java/art/arcane/iris/core/service/StudioSVC.java")).replace("\r\n", "\n");
String builtIn = method(source, "public void downloadBuiltIn(VolmitSender sender, String key)");
String remote = method(source, "public void downloadUrl(VolmitSender sender, String url)");
String execute = method(source, "private void executePackMutation(");
assertTrue(builtIn.contains("PackDownloadProgressReporter reporter"));
assertTrue(remote.contains("PackDownloadProgressReporter reporter"));
assertTrue(remote.contains("reporter::detail"));
assertTrue(remote.contains("\"remote-zip\", reporter"));
assertTrue(remote.contains("cancellation,"));
assertTrue(remote.contains("reporter"));
assertTrue(execute.contains("reporter.cancel();"));
assertTrue(execute.contains("reporter.fail(e);"));
}
@Test
public void shutdownClosesAdmissionAndDrainsTrackedDownloadBeforeServiceTeardown() throws Exception {
String source = Files.readString(Path.of("src/main/java/art/arcane/iris/core/service/StudioSVC.java")).replace("\r\n", "\n");
String onDisable = method(source, "public void onDisable()");
String quiesce = method(source, "public void quiesceDownloadsForShutdown()");
assertTrue(onDisable.contains("quiesceDownloadsForShutdown();"));
assertTrue(quiesce.contains("downloadAdmissionOpen = false;"));
assertTrue(quiesce.contains("execution.cancel();"));
assertTrue(quiesce.contains("execution.await("));
assertTrue(quiesce.contains("while (!execution.isComplete())"));
}
@Test
public void stackedDownloadUsesLocalizedBusyMessage() {
LifecycleOperationCoordinator.ActiveOperation download = new LifecycleOperationCoordinator.ActiveOperation(
@@ -99,24 +32,4 @@ public class StudioSVCPackDownloadContractTest {
StudioSVC.packMutationBusyMessage(worldCreation)
);
}
private static String method(String source, String signature) {
int start = source.indexOf(signature);
assertTrue("Missing source contract signature: " + signature, start >= 0);
int openBrace = source.indexOf('{', start);
assertTrue("Missing source contract method body: " + signature, openBrace >= 0);
int depth = 0;
for (int index = openBrace; index < source.length(); index++) {
char current = source.charAt(index);
if (current == '{') {
depth++;
} else if (current == '}') {
depth--;
if (depth == 0) {
return source.substring(start, index + 1);
}
}
}
throw new IllegalArgumentException("Unclosed source contract method: " + signature);
}
}
@@ -1,35 +0,0 @@
package art.arcane.iris.core.service;
import org.junit.Test;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
public class StudioSVCWorldPackFeedbackContractTest {
@Test
public void persistentPackCopyUsesLifecycleProgressWithoutLegacySnapshotFeedback() throws Exception {
String source = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/service/StudioSVC.java"
)).replace("\r\n", "\n");
assertEquals(2, occurrences(source, "STUDIO_S_V_C_PACK_COPY_REQUIRES_ASYNC_THREAD"));
assertEquals(3, occurrences(source, "STUDIO_S_V_C_PACK_INSTALL_FAILED"));
assertFalse(source.contains("STUDIO_S_V_C_INSTALLING_PACKAGE"));
assertFalse(source.contains("Publishing snapshot"));
assertFalse(source.contains("sender.sendMessage(\"Iris refused to copy a pack"));
assertFalse(source.contains("sender.sendMessage(\"Failed to install studio pack"));
}
private static int occurrences(String value, String match) {
int count = 0;
int offset = 0;
while ((offset = value.indexOf(match, offset)) >= 0) {
count++;
offset += match.length();
}
return count;
}
}
@@ -8,9 +8,11 @@ import art.arcane.iris.core.pack.PackValidationRegistry;
import art.arcane.iris.core.pack.PackValidationResult;
import art.arcane.iris.engine.framework.PreservationRegistry;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.testsupport.PlatformLeakGuard;
import org.junit.Assume;
import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
@@ -32,6 +34,9 @@ import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
public class StudioSVCWorldPackPublishTest {
@ClassRule
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@@ -172,27 +177,6 @@ public class StudioSVCWorldPackPublishTest {
@Test
public void replaceExistingRejectsSymbolicTargetBeforePublication() throws Exception {
String sourceCode = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/service/StudioSVC.java")).replace("\r\n", "\n");
int install = sourceCode.indexOf("private IrisDimension installIntoDirectory(");
int initialTargetSafety = sourceCode.indexOf(
"requireSafePublicationTarget(target, replaceExisting)",
install);
int beginMutation = sourceCode.indexOf(
"PackValidationRegistry.beginRootMutation(target)",
initialTargetSafety);
int finalTargetSafety = sourceCode.indexOf(
"requireSafePublicationTarget(target, replaceExisting)",
beginMutation);
int publish = sourceCode.indexOf(
"AtomicDirectoryPublisher.publish(stage, target)",
finalTargetSafety);
assertTrue(install >= 0);
assertTrue(initialTargetSafety > install);
assertTrue(beginMutation > initialTargetSafety);
assertTrue(finalTargetSafety > beginMutation);
assertTrue(publish > finalTargetSafety);
Path root = temporaryFolder.newFolder("symbolic-replacement-target").toPath();
Path outside = root.resolve("outside-pack");
Path target = root.resolve("world/iris/pack");
@@ -305,27 +289,6 @@ public class StudioSVCWorldPackPublishTest {
@Test
public void replacementKeepsTargetUnauthorizedThroughPublishedFingerprintWindow() throws Exception {
String sourceCode = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/service/StudioSVC.java")).replace("\r\n", "\n");
int install = sourceCode.indexOf("private IrisDimension installIntoDirectory(");
int beginMutation = sourceCode.indexOf("PackValidationRegistry.beginRootMutation(target)", install);
int publish = sourceCode.indexOf("AtomicDirectoryPublisher.publish(stage, target)", beginMutation);
int fingerprint = sourceCode.indexOf(
"ServerConfigurator.computePackTreeFingerprint(target.toFile())",
publish);
int stageValidation = sourceCode.indexOf(
"validatePublishedPack(target, source, copiedFingerprint, validationMutation)",
fingerprint);
int publishCommit = sourceCode.indexOf("publication.commit()", stageValidation);
int validationCommit = sourceCode.indexOf("validationMutation.commit()", publishCommit);
assertTrue(install >= 0);
assertTrue(beginMutation > install);
assertTrue(publish > beginMutation);
assertTrue(fingerprint > publish);
assertTrue(stageValidation > fingerprint);
assertTrue(publishCommit > stageValidation);
assertTrue(validationCommit > publishCommit);
Path root = temporaryFolder.newFolder("validation-publication-window").toPath();
Path sourcePack = root.resolve("source");
Path target = root.resolve("world/iris/pack");
@@ -454,26 +417,6 @@ public class StudioSVCWorldPackPublishTest {
assertEquals("second", second.join());
}
@Test
public void productionWorldPublicationUsesImmutableGenerationHistory() throws Exception {
String source = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/service/StudioSVC.java"
)).replace("\r\n", "\n");
int productionStart = source.indexOf("private IrisDimension publishGenerationHistory(");
int transientStart = source.indexOf("private IrisDimension installIntoDirectory(", productionStart);
String production = source.substring(productionStart, transientStart);
assertTrue(production.contains("GenerationHistory.create("));
assertTrue(production.contains("GenerationHistory.openIfPresent(dimensionRoot, worldSeed)"));
assertTrue(production.contains("GenerationHistory.adoptLegacyPack("));
assertTrue(production.contains("history.stageUpdate("));
assertTrue(production.contains("getGenerationTransitionWidthBlocks()"));
assertTrue(production.contains("history.activePackRoot()"));
assertFalse(production.contains("AtomicDirectoryPublisher.publish("));
assertTrue(source.contains("installIntoTransientWorld("));
assertTrue(source.contains("new File(dimensionRoot, \"iris/pack\")"));
}
private static void writeValidPack(Path packRoot) throws Exception {
Files.createDirectories(packRoot.resolve("dimensions"));
Files.createDirectories(packRoot.resolve("regions"));
@@ -7,8 +7,10 @@ import art.arcane.iris.engine.history.GenerationPackFingerprint;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformStructureHooks;
import art.arcane.iris.testsupport.PlatformLeakGuard;
import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
@@ -30,6 +32,9 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class StructureIndexServiceTest {
@ClassRule
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@@ -4,8 +4,10 @@ import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.framework.MeteredCache;
import art.arcane.iris.engine.framework.PreservationRegistry;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.testsupport.PlatformLeakGuard;
import org.junit.After;
import org.junit.Before;
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.assertArrayEquals;
import static org.junit.Assert.assertFalse;
public class IrisDataStructureRecoveryTest {
@ClassRule
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@@ -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");
@@ -0,0 +1,187 @@
package art.arcane.iris.core.structure.conversion;
import org.junit.Test;
import java.time.Duration;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
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 IrisStructureAdoptionValueTest {
@Test
public void theDefaultLimitsAreTheOnesTheServiceShipsWith() {
IrisStructureAdoptionLimits limits = IrisStructureAdoptionLimits.defaults();
assertEquals(10_000, limits.maxResources());
assertEquals(8 * 1024 * 1024, limits.maxJsonBytes());
assertEquals(64 * 1024 * 1024, limits.maxBinaryBytes());
assertEquals(1024L * 1024L * 1024L, limits.maxTotalBytes());
assertEquals(10_000, limits.maxStructuresScanned());
assertEquals(1_000, limits.maxDiagnostics());
assertEquals(1_024, limits.maxActivePlans());
assertEquals(Duration.ofMinutes(15L), limits.planTtl());
}
@Test
public void everyLimitRejectsBothEndsOfItsRange() {
assertThrows(IllegalArgumentException.class, () -> limits(0, 1, 1, 1L, 1, 1, 1, Duration.ofMinutes(1L)));
assertThrows(IllegalArgumentException.class, () -> limits(100_001, 1, 1, 1L, 1, 1, 1, Duration.ofMinutes(1L)));
assertThrows(IllegalArgumentException.class, () -> limits(1, 0, 1, 1L, 1, 1, 1, Duration.ofMinutes(1L)));
assertThrows(IllegalArgumentException.class, () -> limits(1, 67_108_865, 1, 1L, 1, 1, 1, Duration.ofMinutes(1L)));
assertThrows(IllegalArgumentException.class, () -> limits(1, 1, 0, 1L, 1, 1, 1, Duration.ofMinutes(1L)));
assertThrows(IllegalArgumentException.class, () -> limits(1, 1, 268_435_457, 1L, 1, 1, 1, Duration.ofMinutes(1L)));
assertThrows(IllegalArgumentException.class, () -> limits(1, 1, 1, 0L, 1, 1, 1, Duration.ofMinutes(1L)));
assertThrows(IllegalArgumentException.class, () -> limits(1, 1, 1, 1_073_741_825L, 1, 1, 1, Duration.ofMinutes(1L)));
assertThrows(IllegalArgumentException.class, () -> limits(1, 1, 1, 1L, 0, 1, 1, Duration.ofMinutes(1L)));
assertThrows(IllegalArgumentException.class, () -> limits(1, 1, 1, 1L, 1, 0, 1, Duration.ofMinutes(1L)));
assertThrows(IllegalArgumentException.class, () -> limits(1, 1, 1, 1L, 1, 10_001, 1, Duration.ofMinutes(1L)));
assertThrows(IllegalArgumentException.class, () -> limits(1, 1, 1, 1L, 1, 1, 0, Duration.ofMinutes(1L)));
assertThrows(IllegalArgumentException.class, () -> limits(1, 1, 1, 1L, 1, 1, 10_001, Duration.ofMinutes(1L)));
}
@Test
public void thePlanTtlMustBePositiveAndWithinADay() {
assertThrows(NullPointerException.class, () -> limits(1, 1, 1, 1L, 1, 1, 1, null));
assertThrows(IllegalArgumentException.class, () -> limits(1, 1, 1, 1L, 1, 1, 1, Duration.ZERO));
assertThrows(IllegalArgumentException.class, () -> limits(1, 1, 1, 1L, 1, 1, 1, Duration.ofSeconds(-1L)));
assertThrows(IllegalArgumentException.class,
() -> limits(1, 1, 1, 1L, 1, 1, 1, Duration.ofHours(24L).plusNanos(1L)));
assertEquals(Duration.ofHours(24L), limits(1, 1, 1, 1L, 1, 1, 1, Duration.ofHours(24L)).planTtl());
assertEquals(Duration.ofNanos(1L), limits(1, 1, 1, 1L, 1, 1, 1, Duration.ofNanos(1L)).planTtl());
}
@Test
public void aDiagnosticRendersItsSeverityCodeResourceAndAdvice() {
IrisStructureAdoptionDiagnostic diagnostic = new IrisStructureAdoptionDiagnostic(
IrisStructureAdoptionDiagnostic.Severity.ERROR,
IrisStructureAdoptionDiagnostic.Code.TARGET_RESOURCE_EXISTS,
" structures/village.json ",
"the target already exists",
" pick another target ");
assertEquals("structures/village.json", diagnostic.resource());
assertEquals("ERROR TARGET_RESOURCE_EXISTS [structures/village.json]: "
+ "the target already exists pick another target", diagnostic.summary());
assertTrue(diagnostic.blocking());
}
@Test
public void aDiagnosticWithoutAResourceOrAdviceStillReadsCleanly() {
IrisStructureAdoptionDiagnostic diagnostic = new IrisStructureAdoptionDiagnostic(
IrisStructureAdoptionDiagnostic.Severity.INFO,
IrisStructureAdoptionDiagnostic.Code.IN_PLACE_AVAILABLE,
null,
"the source can be claimed in place",
null);
assertEquals("INFO IN_PLACE_AVAILABLE: the source can be claimed in place", diagnostic.summary());
assertFalse(diagnostic.blocking());
}
@Test
public void aDiagnosticWithoutADetailIsRejected() {
IllegalArgumentException failure = assertThrows(IllegalArgumentException.class,
() -> new IrisStructureAdoptionDiagnostic(
IrisStructureAdoptionDiagnostic.Severity.WARNING,
IrisStructureAdoptionDiagnostic.Code.SHARED_DEPENDENCY,
"structures/village.json",
" ",
null));
assertTrue(failure.getMessage(), failure.getMessage().contains("detail cannot be blank"));
assertThrows(NullPointerException.class, () -> new IrisStructureAdoptionDiagnostic(
null,
IrisStructureAdoptionDiagnostic.Code.SHARED_DEPENDENCY,
null,
"detail",
null));
}
@Test
public void diagnosticsSortErrorsAheadOfWarningsAndInfo() {
IrisStructureAdoptionDiagnostic error = diagnostic(
IrisStructureAdoptionDiagnostic.Severity.ERROR,
IrisStructureAdoptionDiagnostic.Code.PLAN_BLOCKED, "b", "second");
IrisStructureAdoptionDiagnostic warning = diagnostic(
IrisStructureAdoptionDiagnostic.Severity.WARNING,
IrisStructureAdoptionDiagnostic.Code.SHARED_DEPENDENCY, "a", "first");
IrisStructureAdoptionDiagnostic sameSeverityEarlierCode = diagnostic(
IrisStructureAdoptionDiagnostic.Severity.ERROR,
IrisStructureAdoptionDiagnostic.Code.SOURCE_GRAPH_INVALID, "z", "third");
assertTrue(error.compareTo(warning) < 0);
assertTrue(sameSeverityEarlierCode.compareTo(error) < 0);
assertTrue(diagnostic(IrisStructureAdoptionDiagnostic.Severity.ERROR,
IrisStructureAdoptionDiagnostic.Code.PLAN_BLOCKED, "a", "x")
.compareTo(error) < 0);
assertEquals(0, error.compareTo(diagnostic(
IrisStructureAdoptionDiagnostic.Severity.ERROR,
IrisStructureAdoptionDiagnostic.Code.PLAN_BLOCKED, "b", "second")));
}
@Test
public void anUnappliedResultCarriesItsDiagnosticsAndNoReceipt() {
UUID planId = UUID.randomUUID();
IrisStructureAdoptionResult result = new IrisStructureAdoptionResult(
IrisStructureAdoptionResult.Status.BLOCKED,
planId,
List.of(diagnostic(IrisStructureAdoptionDiagnostic.Severity.ERROR,
IrisStructureAdoptionDiagnostic.Code.PLAN_BLOCKED, "structures/village.json", "blocked")),
Optional.empty(),
Optional.empty());
assertFalse(result.successful());
assertEquals(List.of(
"Adoption plan " + planId + ": BLOCKED",
"ERROR PLAN_BLOCKED [structures/village.json]: blocked"), result.summaryLines());
}
@Test
public void anAppliedResultWithoutAReceiptIsRejected() {
IllegalArgumentException failure = assertThrows(IllegalArgumentException.class,
() -> new IrisStructureAdoptionResult(
IrisStructureAdoptionResult.Status.APPLIED,
UUID.randomUUID(),
List.of(),
Optional.empty(),
Optional.empty()));
assertTrue(failure.getMessage(), failure.getMessage().contains("requires a receipt"));
}
private static IrisStructureAdoptionDiagnostic diagnostic(
IrisStructureAdoptionDiagnostic.Severity severity,
IrisStructureAdoptionDiagnostic.Code code,
String resource,
String detail
) {
return new IrisStructureAdoptionDiagnostic(severity, code, resource, detail, null);
}
private static IrisStructureAdoptionLimits limits(
int maxResources,
int maxJsonBytes,
int maxBinaryBytes,
long maxTotalBytes,
int maxStructuresScanned,
int maxDiagnostics,
int maxActivePlans,
Duration planTtl
) {
return new IrisStructureAdoptionLimits(
maxResources,
maxJsonBytes,
maxBinaryBytes,
maxTotalBytes,
maxStructuresScanned,
maxDiagnostics,
maxActivePlans,
planTtl);
}
}
@@ -0,0 +1,73 @@
package art.arcane.iris.core.structure.export;
import art.arcane.volmlib.util.nbt.tag.CompoundTag;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class VanillaBlockStateTest {
@Test
public void aBareBlockKeepsItsNameAndNoProperties() {
VanillaBlockState state = VanillaBlockState.parse(" minecraft:stone ");
assertEquals("minecraft:stone", state.name());
assertTrue(state.properties().isEmpty());
assertEquals("minecraft:stone", state.canonical());
}
@Test
public void propertiesAreCanonicalizedIntoAlphabeticalOrder() {
VanillaBlockState state = VanillaBlockState.parse("minecraft:oak_stairs[waterlogged=false,facing=north,half=top]");
assertEquals("minecraft:oak_stairs[facing=north,half=top,waterlogged=false]", state.canonical());
assertEquals("north", state.properties().get("facing"));
}
@Test
public void theCanonicalFormRoundTripsThroughTheParser() {
String canonical = VanillaBlockState.parse("minecraft:oak_log[axis=z]").canonical();
assertEquals(canonical, VanillaBlockState.parse(canonical).canonical());
}
@Test
public void theNbtFormCarriesTheNameAndOnlyRealProperties() {
CompoundTag bare = VanillaBlockState.parse("minecraft:air").toNbt();
CompoundTag stairs = VanillaBlockState.parse("minecraft:oak_stairs[facing=north]").toNbt();
assertEquals("minecraft:air", bare.getString("Name"));
assertNull(bare.get("Properties"));
assertEquals("minecraft:oak_stairs", stairs.getString("Name"));
assertEquals("north", stairs.getCompoundTag("Properties").getString("facing"));
}
@Test
public void nonMinecraftBlocksAreRejectedBeforeAnythingIsWritten() {
IllegalArgumentException failure =
assertThrows(IllegalArgumentException.class, () -> VanillaBlockState.parse("iris:custom_block"));
assertTrue(failure.getMessage(), failure.getMessage().contains("non-Minecraft block"));
}
@Test
public void malformedPropertyBlocksAreRejected() {
assertThrows(IllegalArgumentException.class, () -> VanillaBlockState.parse(null));
assertThrows(IllegalArgumentException.class, () -> VanillaBlockState.parse("minecraft:stone[facing=north"));
assertThrows(IllegalArgumentException.class, () -> VanillaBlockState.parse("minecraft:stone[]"));
assertThrows(IllegalArgumentException.class, () -> VanillaBlockState.parse("minecraft:stone[facing]"));
assertThrows(IllegalArgumentException.class, () -> VanillaBlockState.parse("minecraft:stone[facing=]"));
assertThrows(IllegalArgumentException.class, () -> VanillaBlockState.parse("minecraft:stone[=north]"));
assertThrows(IllegalArgumentException.class, () -> VanillaBlockState.parse("minecraft:stone[a=1][b=2]"));
}
@Test
public void aRepeatedPropertyIsRejectedInsteadOfSilentlyWinning() {
IllegalArgumentException failure = assertThrows(IllegalArgumentException.class,
() -> VanillaBlockState.parse("minecraft:stone[facing=north,facing=south]"));
assertTrue(failure.getMessage(), failure.getMessage().contains("Duplicate block property"));
}
}
@@ -20,6 +20,7 @@ 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.PlatformLeakGuard;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.nbt.io.NBTUtil;
import art.arcane.volmlib.util.nbt.tag.CompoundTag;
@@ -28,6 +29,7 @@ import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
@@ -53,6 +55,9 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class VanillaJigsawDatapackExporterTest {
@ClassRule
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
private static PlatformBlockState stone;
@Rule
@@ -0,0 +1,124 @@
package art.arcane.iris.core.structure.export;
import art.arcane.iris.engine.framework.structure.StructureGraphResolver;
import art.arcane.iris.engine.object.IrisStructure;
import org.junit.Test;
import java.nio.file.Path;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
public class VanillaJigsawExportRequestTest {
@Test
public void aStructureSourceKeepsTheSuppliedGraphInsteadOfLoadingOne() {
IrisStructure structure = mock(IrisStructure.class);
StructureGraphResolver resolver = mock(StructureGraphResolver.class);
VanillaJigsawExportSource source = VanillaJigsawExportSource.forStructure(" village ", structure, resolver);
assertEquals("village", source.structureKey());
assertSame(structure, source.loadStructure());
assertSame(resolver, source.resolver());
}
@Test
public void aBlankStructureKeyIsRejected() {
IllegalArgumentException failure = assertThrows(IllegalArgumentException.class,
() -> VanillaJigsawExportSource.forStructure(" ", mock(IrisStructure.class), mock(StructureGraphResolver.class)));
assertTrue(failure.getMessage(), failure.getMessage().contains("must not be blank"));
assertThrows(NullPointerException.class,
() -> VanillaJigsawExportSource.forStructure(null, mock(IrisStructure.class), mock(StructureGraphResolver.class)));
}
@Test
public void theResourcePathDefaultsToTheStructureKey() {
VanillaJigsawExportRequest request = request().build();
assertEquals("village", request.resourcePath());
assertEquals("iris", request.namespace());
assertEquals(VanillaJigsawExportFormat.DIRECTORY, request.format());
assertFalse(request.replaceExisting());
}
@Test
public void theOutputPathIsStoredAbsoluteAndNormalized() {
VanillaJigsawExportRequest request = VanillaJigsawExportRequest
.builder(source(), Path.of("build", "exports", "..", "datapack"))
.build();
assertTrue(request.output().isAbsolute());
assertEquals("datapack", request.output().getFileName().toString());
assertFalse(request.output().toString().contains(".."));
}
@Test
public void identityFieldsAreTrimmedButTheDescriptionIsNot() {
VanillaJigsawExportRequest request = request()
.namespace(" mypack ")
.resourcePath(" village/plains ")
.description(" a description ")
.format(VanillaJigsawExportFormat.ZIP)
.replaceExisting(true)
.build();
assertEquals("mypack", request.namespace());
assertEquals("village/plains", request.resourcePath());
assertEquals(" a description ", request.description());
assertEquals(VanillaJigsawExportFormat.ZIP, request.format());
assertTrue(request.replaceExisting());
}
@Test
public void theDefaultSettingsDescribeAVanillaSurfaceStructure() {
VanillaJigsawExportSettings settings = VanillaJigsawExportSettings.defaults();
assertEquals(List.of("minecraft:plains"), settings.biomes());
assertEquals(0, settings.startHeight());
assertEquals(4064, settings.maxDistanceVertical());
assertEquals(32, settings.spacing());
assertEquals(8, settings.separation());
assertEquals(0, settings.salt());
assertEquals(1.0F, settings.frequency(), 0F);
assertFalse(settings.expansionHack());
assertEquals(VanillaJigsawExportSettings.ProjectHeightmap.WORLD_SURFACE_WG, settings.projectHeightmap());
assertEquals(VanillaJigsawExportSettings.GenerationStep.SURFACE_STRUCTURES, settings.generationStep());
assertEquals(VanillaJigsawExportSettings.TerrainAdaptation.NONE, settings.terrainAdaptation());
assertEquals(VanillaJigsawExportSettings.SpreadType.LINEAR, settings.spreadType());
assertNull(VanillaJigsawExportSettings.ProjectHeightmap.NONE.serializedName());
}
@Test
public void theSettingsBuilderReplacesTheBiomeListRatherThanAppendingToIt() {
VanillaJigsawExportSettings settings = VanillaJigsawExportSettings.builder()
.biomes(List.of("minecraft:desert", "minecraft:savanna"))
.biomes(List.of("minecraft:taiga"))
.startHeight(64)
.randomSpread(48, 12, 7)
.frequency(0.5F)
.build();
assertEquals(List.of("minecraft:taiga"), settings.biomes());
assertEquals(64, settings.startHeight());
assertEquals(48, settings.spacing());
assertEquals(12, settings.separation());
assertEquals(7, settings.salt());
assertEquals(0.5F, settings.frequency(), 0F);
}
private static VanillaJigsawExportSource source() {
return VanillaJigsawExportSource.forStructure(
"village", mock(IrisStructure.class), mock(StructureGraphResolver.class));
}
private static VanillaJigsawExportRequest.Builder request() {
return VanillaJigsawExportRequest.builder(source(), Path.of("build", "datapack"));
}
}
@@ -0,0 +1,66 @@
package art.arcane.iris.core.structure.export;
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 VanillaResourceIdentifierTest {
@Test
public void namespacesAcceptOnlyTheVanillaCharacterSet() {
assertTrue(VanillaResourceIdentifier.validNamespace("minecraft"));
assertTrue(VanillaResourceIdentifier.validNamespace("iris_pack-1.0"));
assertFalse(VanillaResourceIdentifier.validNamespace(null));
assertFalse(VanillaResourceIdentifier.validNamespace(""));
assertFalse(VanillaResourceIdentifier.validNamespace("Minecraft"));
assertFalse(VanillaResourceIdentifier.validNamespace("iris/pack"));
}
@Test
public void pathsAllowNestingButNeverTraversal() {
assertTrue(VanillaResourceIdentifier.validPath("village/plains/houses"));
assertTrue(VanillaResourceIdentifier.validPath("empty"));
assertFalse(VanillaResourceIdentifier.validPath("/village"));
assertFalse(VanillaResourceIdentifier.validPath("village/"));
assertFalse(VanillaResourceIdentifier.validPath("village//houses"));
assertFalse(VanillaResourceIdentifier.validPath("village/../secrets"));
assertFalse(VanillaResourceIdentifier.validPath("village/./houses"));
assertFalse(VanillaResourceIdentifier.validPath("Village"));
}
@Test
public void identifiersRequireExactlyOneSeparatorWithBothHalvesPresent() {
assertTrue(VanillaResourceIdentifier.validIdentifier("minecraft:village/plains"));
assertFalse(VanillaResourceIdentifier.validIdentifier("village"));
assertFalse(VanillaResourceIdentifier.validIdentifier(":village"));
assertFalse(VanillaResourceIdentifier.validIdentifier("minecraft:"));
assertFalse(VanillaResourceIdentifier.validIdentifier("minecraft:village:plains"));
assertFalse(VanillaResourceIdentifier.validIdentifier(null));
}
@Test
public void anAbsentConnectorTargetBecomesTheVanillaEmptyPool() {
assertEquals("minecraft:empty", VanillaResourceIdentifier.normalizeConnectorIdentifier(null));
assertEquals("minecraft:empty", VanillaResourceIdentifier.normalizeConnectorIdentifier(""));
assertEquals("minecraft:empty", VanillaResourceIdentifier.normalizeConnectorIdentifier(" "));
}
@Test
public void abareConnectorNameIsQualifiedIntoTheMinecraftNamespace() {
assertEquals("minecraft:bottom", VanillaResourceIdentifier.normalizeConnectorIdentifier("bottom"));
assertEquals("minecraft:bottom", VanillaResourceIdentifier.normalizeConnectorIdentifier(" bottom "));
assertEquals("iris:side", VanillaResourceIdentifier.normalizeConnectorIdentifier("iris:side"));
}
@Test
public void aMalformedConnectorTargetIsRejectedWithItsOriginalText() {
IllegalArgumentException failure = assertThrows(IllegalArgumentException.class,
() -> VanillaResourceIdentifier.normalizeConnectorIdentifier("Iris:Side"));
assertTrue(failure.getMessage(), failure.getMessage().contains("Iris:Side"));
assertThrows(IllegalArgumentException.class,
() -> VanillaResourceIdentifier.normalizeConnectorIdentifier("iris:../escape"));
}
}
Loaded 100 of 520 files, more files were not shown because too many files have changed in this diff. Show more