mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-25 21:40:05 +00:00
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:
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 {
|
||||
|
||||
-37
@@ -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);
|
||||
}
|
||||
|
||||
-51
@@ -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;
|
||||
|
||||
+66
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+110
@@ -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
@@ -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();
|
||||
|
||||
|
||||
+2
-1
@@ -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();
|
||||
|
||||
|
||||
+151
-157
@@ -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();
|
||||
|
||||
|
||||
+47
@@ -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);
|
||||
}
|
||||
}
|
||||
+40
@@ -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) {
|
||||
}
|
||||
}
|
||||
}
|
||||
+152
@@ -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");
|
||||
}
|
||||
}
|
||||
+37
@@ -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());
|
||||
}
|
||||
}
|
||||
+136
@@ -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);
|
||||
}
|
||||
}
|
||||
+111
@@ -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);
|
||||
}
|
||||
}
|
||||
+194
@@ -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);
|
||||
}
|
||||
}
|
||||
+43
@@ -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());
|
||||
}
|
||||
}
|
||||
Vendored
+5
@@ -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<>();
|
||||
|
||||
|
||||
+9
-7
@@ -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();
|
||||
}
|
||||
|
||||
-144
@@ -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())
|
||||
|
||||
-209
@@ -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)"));
|
||||
}
|
||||
}
|
||||
-24
@@ -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)"));
|
||||
}
|
||||
}
|
||||
+33
@@ -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);
|
||||
|
||||
+8
-4
@@ -6,6 +6,7 @@ import art.arcane.iris.engine.object.IrisJigsawPiece;
|
||||
import art.arcane.iris.engine.object.IrisObject;
|
||||
import art.arcane.iris.engine.object.IrisPosition;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.testsupport.DurabilityMode;
|
||||
import art.arcane.iris.util.common.math.IrisBlockVector;
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonObject;
|
||||
@@ -36,6 +37,9 @@ import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class JigsawStudioGraphEditorResizeTest {
|
||||
@ClassRule
|
||||
public static final DurabilityMode DURABILITY = DurabilityMode.relaxed();
|
||||
|
||||
private static final String STRUCTURE_KEY = "resize/planar";
|
||||
|
||||
@ClassRule
|
||||
@@ -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,
|
||||
|
||||
+4
@@ -10,6 +10,7 @@ import art.arcane.iris.core.structure.authoring.StructureWriteMode;
|
||||
import art.arcane.iris.core.structure.authoring.StructureWriteResult;
|
||||
import art.arcane.iris.engine.object.IrisJigsawThemeSet;
|
||||
import art.arcane.iris.engine.object.IrisObject;
|
||||
import art.arcane.iris.testsupport.DurabilityMode;
|
||||
import art.arcane.iris.util.common.math.IrisBlockVector;
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
@@ -40,6 +41,9 @@ import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class JigsawStudioPersistenceEditorsTest {
|
||||
@ClassRule
|
||||
public static final DurabilityMode DURABILITY = DurabilityMode.relaxed();
|
||||
|
||||
private static final String PLANAR_KEY = "studio/planar";
|
||||
private static final String SPATIAL_KEY = "studio/spatial";
|
||||
|
||||
|
||||
+4
@@ -9,6 +9,7 @@ import art.arcane.iris.engine.framework.structure.StructureGraphCompilation;
|
||||
import art.arcane.iris.engine.framework.structure.StructureResourceBundleGraphCompiler;
|
||||
import art.arcane.iris.engine.object.IrisObject;
|
||||
import art.arcane.iris.engine.object.IrisPosition;
|
||||
import art.arcane.iris.testsupport.DurabilityMode;
|
||||
import art.arcane.iris.util.common.math.IrisBlockVector;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import com.google.gson.JsonObject;
|
||||
@@ -35,6 +36,9 @@ import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
public class JigsawStudioProjectCreatorTest {
|
||||
@ClassRule
|
||||
public static final DurabilityMode DURABILITY = DurabilityMode.relaxed();
|
||||
|
||||
private static final JigsawStudioProjectCreator.Options DEFAULT_PLANAR_OPTIONS =
|
||||
new JigsawStudioProjectCreator.Options(
|
||||
"settlement/test",
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
+75
-69
@@ -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();
|
||||
|
||||
+213
@@ -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();
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -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();
|
||||
|
||||
|
||||
+4
@@ -17,6 +17,7 @@ import art.arcane.iris.engine.object.IrisJigsawPiece;
|
||||
import art.arcane.iris.engine.object.IrisObject;
|
||||
import art.arcane.iris.engine.object.IrisPosition;
|
||||
import art.arcane.iris.engine.object.JigsawJoint;
|
||||
import art.arcane.iris.testsupport.DurabilityMode;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonObject;
|
||||
@@ -46,6 +47,9 @@ import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class JigsawStudioResourceBundleAssemblerTest {
|
||||
@ClassRule
|
||||
public static final DurabilityMode DURABILITY = DurabilityMode.relaxed();
|
||||
|
||||
private static final Gson PRETTY_GSON = new GsonBuilder().setPrettyPrinting().create();
|
||||
private static final String STRUCTURE_KEY = "fort";
|
||||
|
||||
|
||||
+169
-164
@@ -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());
|
||||
|
||||
+19
-15
@@ -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()) {
|
||||
|
||||
-87
@@ -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);
|
||||
}
|
||||
}
|
||||
-35
@@ -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();
|
||||
|
||||
|
||||
+5
@@ -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();
|
||||
|
||||
|
||||
+5
@@ -18,6 +18,8 @@
|
||||
|
||||
package art.arcane.iris.core.structure.authoring;
|
||||
|
||||
import art.arcane.iris.testsupport.DurabilityMode;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.Assume;
|
||||
@@ -40,6 +42,9 @@ import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class StructureTransactionWriterTest {
|
||||
@ClassRule
|
||||
public static final DurabilityMode DURABILITY = DurabilityMode.relaxed();
|
||||
|
||||
private static final StructureKey TARGET_KEY = StructureKey.parse("iris_test:temple");
|
||||
private static final StructureKey SOURCE_KEY = StructureKey.parse("minecraft:trial_chambers");
|
||||
|
||||
|
||||
+187
@@ -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"));
|
||||
}
|
||||
}
|
||||
+5
@@ -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
|
||||
|
||||
+124
@@ -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"));
|
||||
}
|
||||
}
|
||||
+66
@@ -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
Reference in new issue
Block a user