From 290412b62a7029431bc5bf96c64e46fea13ba430 Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Tue, 8 Sep 2026 05:16:40 -0400 Subject: [PATCH] Cover untested contract surfaces - Add a test source set to spi with bind, service registry and platform value contracts - Prove the DataType probe contract, the ExternalDataProvider defaults and the string-only provider namespace claims - Prove Identifier parsing and rendering - Cover the data version, primitive and vector director handlers - Cover the vanilla export identifier, block state, orientation, request and settings values - Cover the structure adoption limits, diagnostics and result invariants - Cover the pregen mantle backpressure, heap reclaim urgency, empty cache, listener defaults and diagnostic descriptions --- .../arcane/iris/core/link/IdentifierTest.java | 48 +++++ .../link/data/DataTypeProbeContractTest.java | 156 +++++++++++++++ .../ExternalDataProviderDefaultsTest.java | 110 +++++++++++ .../link/data/ProviderNamespaceClaimTest.java | 90 +++++++++ .../MantleHeapPressureUrgencyTest.java | 47 +++++ .../PregenDiagnosticsDescriptionTest.java | 40 ++++ .../PregenListenerDefaultsTest.java | 104 ++++++++++ .../PregenMantleBackpressureTest.java | 152 ++++++++++++++ .../cache/PregenCacheDefaultsTest.java | 43 ++++ .../IrisStructureAdoptionValueTest.java | 187 ++++++++++++++++++ .../export/VanillaBlockStateTest.java | 73 +++++++ .../VanillaJigsawExportRequestTest.java | 124 ++++++++++++ .../export/VanillaResourceIdentifierTest.java | 66 +++++++ ...nillaStructureTemplateOrientationTest.java | 50 +++++ .../handlers/DataVersionHandlerTest.java | 56 ++++++ .../handlers/PrimitiveHandlerParsingTest.java | 94 +++++++++ .../handlers/VectorHandlerParsingTest.java | 144 ++++++++++++++ spi/build.gradle | 4 + .../spi/IrisPlatformsBindContractTest.java | 172 ++++++++++++++++ .../spi/IrisServicesRegistryContractTest.java | 93 +++++++++ .../iris/spi/PlatformValueContractTest.java | 177 +++++++++++++++++ 21 files changed, 2030 insertions(+) create mode 100644 core/src/test/java/art/arcane/iris/core/link/IdentifierTest.java create mode 100644 core/src/test/java/art/arcane/iris/core/link/data/DataTypeProbeContractTest.java create mode 100644 core/src/test/java/art/arcane/iris/core/link/data/ExternalDataProviderDefaultsTest.java create mode 100644 core/src/test/java/art/arcane/iris/core/link/data/ProviderNamespaceClaimTest.java create mode 100644 core/src/test/java/art/arcane/iris/core/pregenerator/MantleHeapPressureUrgencyTest.java create mode 100644 core/src/test/java/art/arcane/iris/core/pregenerator/PregenDiagnosticsDescriptionTest.java create mode 100644 core/src/test/java/art/arcane/iris/core/pregenerator/PregenListenerDefaultsTest.java create mode 100644 core/src/test/java/art/arcane/iris/core/pregenerator/PregenMantleBackpressureTest.java create mode 100644 core/src/test/java/art/arcane/iris/core/pregenerator/cache/PregenCacheDefaultsTest.java create mode 100644 core/src/test/java/art/arcane/iris/core/structure/conversion/IrisStructureAdoptionValueTest.java create mode 100644 core/src/test/java/art/arcane/iris/core/structure/export/VanillaBlockStateTest.java create mode 100644 core/src/test/java/art/arcane/iris/core/structure/export/VanillaJigsawExportRequestTest.java create mode 100644 core/src/test/java/art/arcane/iris/core/structure/export/VanillaResourceIdentifierTest.java create mode 100644 core/src/test/java/art/arcane/iris/core/structure/export/VanillaStructureTemplateOrientationTest.java create mode 100644 core/src/test/java/art/arcane/iris/util/common/director/handlers/DataVersionHandlerTest.java create mode 100644 core/src/test/java/art/arcane/iris/util/common/director/handlers/PrimitiveHandlerParsingTest.java create mode 100644 core/src/test/java/art/arcane/iris/util/common/director/handlers/VectorHandlerParsingTest.java create mode 100644 spi/src/test/java/art/arcane/iris/spi/IrisPlatformsBindContractTest.java create mode 100644 spi/src/test/java/art/arcane/iris/spi/IrisServicesRegistryContractTest.java create mode 100644 spi/src/test/java/art/arcane/iris/spi/PlatformValueContractTest.java diff --git a/core/src/test/java/art/arcane/iris/core/link/IdentifierTest.java b/core/src/test/java/art/arcane/iris/core/link/IdentifierTest.java new file mode 100644 index 000000000..c3ddcd990 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/core/link/IdentifierTest.java @@ -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")); + } +} diff --git a/core/src/test/java/art/arcane/iris/core/link/data/DataTypeProbeContractTest.java b/core/src/test/java/art/arcane/iris/core/link/data/DataTypeProbeContractTest.java new file mode 100644 index 000000000..b61bfc101 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/core/link/data/DataTypeProbeContractTest.java @@ -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 claimed = List.of(RUBY, new Identifier("other", "quartz")); + + List 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 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 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 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 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; + } + } +} diff --git a/core/src/test/java/art/arcane/iris/core/link/data/ExternalDataProviderDefaultsTest.java b/core/src/test/java/art/arcane/iris/core/link/data/ExternalDataProviderDefaultsTest.java new file mode 100644 index 000000000..a71f0bf87 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/core/link/data/ExternalDataProviderDefaultsTest.java @@ -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 calls = new ArrayList<>(); + + @Override + public @NotNull BlockData getBlockData(@NotNull Identifier blockId, @NotNull KMap state) { + calls.add("block:" + state.size()); + return mock(BlockData.class); + } + + @Override + public @NotNull ItemStack getItemStack(@NotNull Identifier itemId, @NotNull KMap customNbt) { + calls.add("item:" + customNbt.size()); + return mock(ItemStack.class); + } + } +} diff --git a/core/src/test/java/art/arcane/iris/core/link/data/ProviderNamespaceClaimTest.java b/core/src/test/java/art/arcane/iris/core/link/data/ProviderNamespaceClaimTest.java new file mode 100644 index 000000000..a99c4b957 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/core/link/data/ProviderNamespaceClaimTest.java @@ -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()); + } +} diff --git a/core/src/test/java/art/arcane/iris/core/pregenerator/MantleHeapPressureUrgencyTest.java b/core/src/test/java/art/arcane/iris/core/pregenerator/MantleHeapPressureUrgencyTest.java new file mode 100644 index 000000000..d530d419e --- /dev/null +++ b/core/src/test/java/art/arcane/iris/core/pregenerator/MantleHeapPressureUrgencyTest.java @@ -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); + } +} diff --git a/core/src/test/java/art/arcane/iris/core/pregenerator/PregenDiagnosticsDescriptionTest.java b/core/src/test/java/art/arcane/iris/core/pregenerator/PregenDiagnosticsDescriptionTest.java new file mode 100644 index 000000000..5c9c929e2 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/core/pregenerator/PregenDiagnosticsDescriptionTest.java @@ -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(""))); + } +} diff --git a/core/src/test/java/art/arcane/iris/core/pregenerator/PregenListenerDefaultsTest.java b/core/src/test/java/art/arcane/iris/core/pregenerator/PregenListenerDefaultsTest.java new file mode 100644 index 000000000..368bc8709 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/core/pregenerator/PregenListenerDefaultsTest.java @@ -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 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) { + } + } +} diff --git a/core/src/test/java/art/arcane/iris/core/pregenerator/PregenMantleBackpressureTest.java b/core/src/test/java/art/arcane/iris/core/pregenerator/PregenMantleBackpressureTest.java new file mode 100644 index 000000000..5ddaedfaf --- /dev/null +++ b/core/src/test/java/art/arcane/iris/core/pregenerator/PregenMantleBackpressureTest.java @@ -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 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 mantleSupplier, + int maxResidentTectonicPlates, + long timeoutMs, + Runnable onBudgetTimeout + ) { + return new PregenMantleBackpressure( + mantleSupplier, maxResidentTectonicPlates, 5, timeoutMs, onBudgetTimeout, () -> "test"); + } +} diff --git a/core/src/test/java/art/arcane/iris/core/pregenerator/cache/PregenCacheDefaultsTest.java b/core/src/test/java/art/arcane/iris/core/pregenerator/cache/PregenCacheDefaultsTest.java new file mode 100644 index 000000000..c897901ee --- /dev/null +++ b/core/src/test/java/art/arcane/iris/core/pregenerator/cache/PregenCacheDefaultsTest.java @@ -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()); + } +} diff --git a/core/src/test/java/art/arcane/iris/core/structure/conversion/IrisStructureAdoptionValueTest.java b/core/src/test/java/art/arcane/iris/core/structure/conversion/IrisStructureAdoptionValueTest.java new file mode 100644 index 000000000..1097812fa --- /dev/null +++ b/core/src/test/java/art/arcane/iris/core/structure/conversion/IrisStructureAdoptionValueTest.java @@ -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); + } +} diff --git a/core/src/test/java/art/arcane/iris/core/structure/export/VanillaBlockStateTest.java b/core/src/test/java/art/arcane/iris/core/structure/export/VanillaBlockStateTest.java new file mode 100644 index 000000000..f5d42318d --- /dev/null +++ b/core/src/test/java/art/arcane/iris/core/structure/export/VanillaBlockStateTest.java @@ -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")); + } +} diff --git a/core/src/test/java/art/arcane/iris/core/structure/export/VanillaJigsawExportRequestTest.java b/core/src/test/java/art/arcane/iris/core/structure/export/VanillaJigsawExportRequestTest.java new file mode 100644 index 000000000..7ead6f8df --- /dev/null +++ b/core/src/test/java/art/arcane/iris/core/structure/export/VanillaJigsawExportRequestTest.java @@ -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")); + } +} diff --git a/core/src/test/java/art/arcane/iris/core/structure/export/VanillaResourceIdentifierTest.java b/core/src/test/java/art/arcane/iris/core/structure/export/VanillaResourceIdentifierTest.java new file mode 100644 index 000000000..c2552746f --- /dev/null +++ b/core/src/test/java/art/arcane/iris/core/structure/export/VanillaResourceIdentifierTest.java @@ -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")); + } +} diff --git a/core/src/test/java/art/arcane/iris/core/structure/export/VanillaStructureTemplateOrientationTest.java b/core/src/test/java/art/arcane/iris/core/structure/export/VanillaStructureTemplateOrientationTest.java new file mode 100644 index 000000000..89e81f0d6 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/core/structure/export/VanillaStructureTemplateOrientationTest.java @@ -0,0 +1,50 @@ +package art.arcane.iris.core.structure.export; + +import art.arcane.iris.engine.object.IrisDirection; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +public class VanillaStructureTemplateOrientationTest { + @Test + public void aHorizontalFrontIsRenderedAgainstAnUpwardTop() { + assertEquals("north_up", VanillaStructureTemplateEncoder.orientation( + IrisDirection.NORTH_NEGATIVE_Z, IrisDirection.UP_POSITIVE_Y)); + assertEquals("south_up", VanillaStructureTemplateEncoder.orientation( + IrisDirection.SOUTH_POSITIVE_Z, IrisDirection.UP_POSITIVE_Y)); + assertEquals("east_up", VanillaStructureTemplateEncoder.orientation( + IrisDirection.EAST_POSITIVE_X, IrisDirection.UP_POSITIVE_Y)); + assertEquals("west_up", VanillaStructureTemplateEncoder.orientation( + IrisDirection.WEST_NEGATIVE_X, IrisDirection.UP_POSITIVE_Y)); + } + + @Test + public void averticalFrontIsRenderedAgainstItsHorizontalTop() { + assertEquals("up_north", VanillaStructureTemplateEncoder.orientation( + IrisDirection.UP_POSITIVE_Y, IrisDirection.NORTH_NEGATIVE_Z)); + assertEquals("down_east", VanillaStructureTemplateEncoder.orientation( + IrisDirection.DOWN_NEGATIVE_Y, IrisDirection.EAST_POSITIVE_X)); + } + + @Test + public void averticalFrontCannotTakeAVerticalTop() { + IllegalArgumentException failure = assertThrows(IllegalArgumentException.class, + () -> VanillaStructureTemplateEncoder.orientation( + IrisDirection.UP_POSITIVE_Y, IrisDirection.UP_POSITIVE_Y)); + + assertTrue(failure.getMessage(), failure.getMessage().contains("horizontal top direction")); + } + + @Test + public void aHorizontalFrontCannotTakeANonUpwardTop() { + IllegalArgumentException failure = assertThrows(IllegalArgumentException.class, + () -> VanillaStructureTemplateEncoder.orientation( + IrisDirection.NORTH_NEGATIVE_Z, IrisDirection.SOUTH_POSITIVE_Z)); + + assertTrue(failure.getMessage(), failure.getMessage().contains("upward top direction")); + assertThrows(IllegalArgumentException.class, () -> VanillaStructureTemplateEncoder.orientation( + IrisDirection.EAST_POSITIVE_X, IrisDirection.DOWN_NEGATIVE_Y)); + } +} diff --git a/core/src/test/java/art/arcane/iris/util/common/director/handlers/DataVersionHandlerTest.java b/core/src/test/java/art/arcane/iris/util/common/director/handlers/DataVersionHandlerTest.java new file mode 100644 index 000000000..48770c6dc --- /dev/null +++ b/core/src/test/java/art/arcane/iris/util/common/director/handlers/DataVersionHandlerTest.java @@ -0,0 +1,56 @@ +package art.arcane.iris.util.common.director.handlers; + +import art.arcane.iris.core.nms.datapack.DataVersion; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.director.exceptions.DirectorParsingException; +import org.junit.Test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +public class DataVersionHandlerTest { + private final DataVersionHandler handler = new DataVersionHandler(); + + @Test + public void latestResolvesToTheNewestShippedDatapackVersion() throws DirectorParsingException { + assertSame(DataVersion.getLatest(), handler.parse("latest", false)); + assertSame(DataVersion.getLatest(), handler.parse("LATEST", false)); + } + + @Test + public void anExactVersionStringResolvesRegardlessOfCase() throws DirectorParsingException { + assertSame(DataVersion.V26_2, handler.parse("26.2", false)); + assertSame(DataVersion.V26_1_2, handler.parse("26.1.2", false)); + } + + @Test + public void theUnsupportedSentinelIsHiddenFromSuggestionsButStillParses() throws DirectorParsingException { + KList possibilities = handler.getPossibilities(); + + assertFalse(possibilities.contains(DataVersion.UNSUPPORTED)); + assertTrue(possibilities.contains(DataVersion.getLatest())); + assertSame(DataVersion.UNSUPPORTED, handler.parse(DataVersion.UNSUPPORTED.getVersion(), false)); + } + + @Test + public void anUnknownVersionNamesTheOffendingInput() { + DirectorParsingException failure = + assertThrows(DirectorParsingException.class, () -> handler.parse("nope", false)); + + assertTrue(failure.getMessage(), failure.getMessage().contains("nope")); + } + + @Test + public void theRenderedFormIsTheVersionString() { + assertSame(DataVersion.V26_2.getVersion(), handler.toString(DataVersion.V26_2)); + } + + @Test + public void onlyDataVersionParametersAreClaimed() { + assertTrue(handler.supports(DataVersion.class)); + assertFalse(handler.supports(String.class)); + assertFalse(handler.supports(Integer.class)); + } +} diff --git a/core/src/test/java/art/arcane/iris/util/common/director/handlers/PrimitiveHandlerParsingTest.java b/core/src/test/java/art/arcane/iris/util/common/director/handlers/PrimitiveHandlerParsingTest.java new file mode 100644 index 000000000..59ea36282 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/util/common/director/handlers/PrimitiveHandlerParsingTest.java @@ -0,0 +1,94 @@ +package art.arcane.iris.util.common.director.handlers; + +import art.arcane.volmlib.util.director.exceptions.DirectorParsingException; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +public class PrimitiveHandlerParsingTest { + @Test + public void integerSuffixesScaleTheParsedValue() throws DirectorParsingException { + IntegerHandler handler = new IntegerHandler(); + + assertEquals(Integer.valueOf(5), handler.parse("5", false)); + assertEquals(Integer.valueOf(64), handler.parse("4c", false)); + assertEquals(Integer.valueOf(1_000), handler.parse("1k", false)); + assertEquals(Integer.valueOf(200), handler.parse("2h", false)); + assertEquals(Integer.valueOf(512), handler.parse("1r", false)); + } + + @Test + public void stackedIntegerSuffixesMultiplyTogether() throws DirectorParsingException { + assertEquals(Integer.valueOf(2_000_000), new IntegerHandler().parse("2kk", false)); + } + + @Test + public void anUnparseableIntegerNamesTheOffendingInput() { + DirectorParsingException failure = + assertThrows(DirectorParsingException.class, () -> new IntegerHandler().parse("abc", false)); + + assertTrue(failure.getMessage(), failure.getMessage().contains("abc")); + } + + @Test + public void anUnscaledLongKeepsFullPrecision() throws DirectorParsingException { + LongHandler handler = new LongHandler(); + + assertEquals(Long.valueOf(9_007_199_254_740_993L), handler.parse("9007199254740993", false)); + assertEquals(Long.valueOf(1_000L), handler.parse("1k", false)); + } + + @Test + public void shortAndFloatingPointHandlersShareTheSuffixTable() throws DirectorParsingException { + assertEquals(Short.valueOf((short) 32), new ShortHandler().parse("2c", false)); + assertEquals(1_500F, new FloatHandler().parse("1.5k", false), 0F); + assertEquals(1_500D, new DoubleHandler().parse("1.5k", false), 0D); + } + + @Test + public void byteParsingHasNoSuffixTable() throws DirectorParsingException { + ByteHandler handler = new ByteHandler(); + + assertEquals(Byte.valueOf((byte) 12), handler.parse("12", false)); + assertThrows(DirectorParsingException.class, () -> handler.parse("1k", false)); + } + + @Test + public void booleanSentinelsAreCaseSensitiveAndEverythingElseFallsBackToFalse() throws DirectorParsingException { + BooleanHandler handler = new BooleanHandler(); + + assertNull(handler.parse("null", false)); + assertNull(handler.parse("other", false)); + assertNull(handler.parse("flip", false)); + assertEquals(Boolean.TRUE, handler.parse("TRUE", false)); + assertEquals(Boolean.FALSE, handler.parse("yes", false)); + assertEquals(Boolean.FALSE, handler.parse("NULL", false)); + } + + @Test + public void stringParsingIsVerbatim() throws DirectorParsingException { + StringHandler handler = new StringHandler(); + + assertEquals(" spaced ", handler.parse(" spaced ", false)); + assertEquals("", handler.parse("", false)); + } + + @Test + public void eachHandlerClaimsOnlyItsOwnParameterType() { + assertTrue(new IntegerHandler().supports(int.class)); + assertTrue(new IntegerHandler().supports(Integer.class)); + assertFalse(new IntegerHandler().supports(Long.class)); + assertTrue(new LongHandler().supports(long.class)); + assertTrue(new ShortHandler().supports(short.class)); + assertTrue(new ByteHandler().supports(byte.class)); + assertTrue(new FloatHandler().supports(float.class)); + assertTrue(new DoubleHandler().supports(double.class)); + assertTrue(new BooleanHandler().supports(boolean.class)); + assertTrue(new StringHandler().supports(String.class)); + assertFalse(new StringHandler().supports(Object.class)); + } +} diff --git a/core/src/test/java/art/arcane/iris/util/common/director/handlers/VectorHandlerParsingTest.java b/core/src/test/java/art/arcane/iris/util/common/director/handlers/VectorHandlerParsingTest.java new file mode 100644 index 000000000..9e4d81c44 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/util/common/director/handlers/VectorHandlerParsingTest.java @@ -0,0 +1,144 @@ +package art.arcane.iris.util.common.director.handlers; + +import art.arcane.volmlib.util.director.exceptions.DirectorParsingException; +import org.bukkit.Location; +import org.bukkit.entity.Player; +import org.bukkit.util.Vector; +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class VectorHandlerParsingTest { + @Test + public void threeComponentsMapToTheirAxes() throws DirectorParsingException { + Vector parsed = console().parse("1, 2 ,3", false); + + assertEquals(1D, parsed.getX(), 0D); + assertEquals(2D, parsed.getY(), 0D); + assertEquals(3D, parsed.getZ(), 0D); + } + + @Test + public void twoComponentsAreTreatedAsAHorizontalPair() throws DirectorParsingException { + Vector parsed = console().parse("4,6", false); + + assertEquals(4D, parsed.getX(), 0D); + assertEquals(0D, parsed.getY(), 0D); + assertEquals(6D, parsed.getZ(), 0D); + } + + @Test + public void anyOtherComponentCountIsRejectedWithItsArity() { + DirectorParsingException failure = + assertThrows(DirectorParsingException.class, () -> console().parse("1,2,3,4", false)); + + assertTrue(failure.getMessage(), failure.getMessage().contains("4 components")); + assertTrue(failure.getMessage(), failure.getMessage().contains("Expected 2 or 3")); + } + + @Test + public void positionalKeywordsAreRefusedForAConsoleSender() { + VectorHandler handler = console(); + + assertTrue(assertThrows(DirectorParsingException.class, () -> handler.parse("here", false)) + .getMessage().contains("as a console")); + assertTrue(assertThrows(DirectorParsingException.class, () -> handler.parse("SELF", false)) + .getMessage().contains("as a console")); + assertTrue(assertThrows(DirectorParsingException.class, () -> handler.parse("look", false)) + .getMessage().contains("as a console")); + assertTrue(assertThrows(DirectorParsingException.class, () -> handler.parse("Crosshair", false)) + .getMessage().contains("as a console")); + } + + @Test + public void positionalKeywordsResolveForAPlayerSender() throws DirectorParsingException { + VectorHandler handler = new TestVectorHandler(true, new Vector(10, 20, 30), new Vector(-1, -2, -3), List.of()); + + assertEquals(new Vector(10, 20, 30), handler.parse("me", false)); + assertEquals(new Vector(-1, -2, -3), handler.parse("cursor", false)); + } + + @Test + public void aNamedPlayerResolvesToThatPlayersPosition() throws DirectorParsingException { + Player player = mock(Player.class); + when(player.getLocation()).thenReturn(new Location(null, 7D, 8D, 9D)); + VectorHandler handler = new TestVectorHandler(false, null, null, List.of(player)); + + assertEquals(new Vector(7, 8, 9), handler.parse("player:steve", false)); + } + + @Test + public void anUnmatchedPlayerQueryNamesTheQuery() { + VectorHandler handler = new TestVectorHandler(false, null, null, List.of()); + + DirectorParsingException failure = + assertThrows(DirectorParsingException.class, () -> handler.parse("player:steve", false)); + + assertTrue(failure.getMessage(), failure.getMessage().contains("steve")); + } + + @Test + public void anUnrecognizedTokenResolvesToNothingRatherThanFailing() throws DirectorParsingException { + assertNull(console().parse("garbage", false)); + assertNull(console().parse("", false)); + } + + @Test + public void theRenderedFormOmitsAZeroHeight() { + VectorHandler handler = console(); + + assertEquals(2, handler.toString(new Vector(1, 0, 3)).split(",").length); + assertEquals(3, handler.toString(new Vector(1, 2, 3)).split(",").length); + } + + @Test + public void onlyVectorParametersAreClaimed() { + assertTrue(console().supports(Vector.class)); + assertEquals(false, console().supports(Location.class)); + } + + private static VectorHandler console() { + return new TestVectorHandler(false, null, null, List.of()); + } + + private static final class TestVectorHandler extends VectorHandler { + private final boolean senderIsPlayer; + private final Vector senderVector; + private final Vector lookVector; + private final List players; + + private TestVectorHandler(boolean senderIsPlayer, Vector senderVector, Vector lookVector, List players) { + this.senderIsPlayer = senderIsPlayer; + this.senderVector = senderVector; + this.lookVector = lookVector; + this.players = players; + } + + @Override + protected boolean isSenderPlayer() { + return senderIsPlayer; + } + + @Override + protected Vector getSenderVector() { + return senderVector; + } + + @Override + protected Vector getLookVector() { + return lookVector; + } + + @Override + protected List playerPossibilities(String query) { + return players; + } + } +} diff --git a/spi/build.gradle b/spi/build.gradle index 019cf2024..39b8096ca 100644 --- a/spi/build.gradle +++ b/spi/build.gradle @@ -11,6 +11,10 @@ java { } } +dependencies { + testImplementation('junit:junit:4.13.2') +} + tasks.named('jar', Jar).configure { archiveBaseName.set('iris-spi') } diff --git a/spi/src/test/java/art/arcane/iris/spi/IrisPlatformsBindContractTest.java b/spi/src/test/java/art/arcane/iris/spi/IrisPlatformsBindContractTest.java new file mode 100644 index 000000000..4a34bced2 --- /dev/null +++ b/spi/src/test/java/art/arcane/iris/spi/IrisPlatformsBindContractTest.java @@ -0,0 +1,172 @@ +package art.arcane.iris.spi; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; + +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; + +public class IrisPlatformsBindContractTest { + @Before + public void requireUnbound() { + IrisPlatforms.unbind(); + } + + @After + public void releaseBinding() { + IrisPlatforms.unbind(); + } + + @Test + public void unboundLookupsReportAbsenceInsteadOfImprovising() { + assertFalse(IrisPlatforms.isBound()); + assertNull(IrisPlatforms.getOrNull()); + + IllegalStateException failure = assertThrows(IllegalStateException.class, IrisPlatforms::get); + + assertTrue(failure.getMessage(), failure.getMessage().contains("No Iris platform is bound")); + } + + @Test + public void bindingIsVisibleThroughEveryAccessor() { + IrisPlatform platform = new StubPlatform(); + + IrisPlatforms.bind(platform); + + assertTrue(IrisPlatforms.isBound()); + assertSame(platform, IrisPlatforms.get()); + assertSame(platform, IrisPlatforms.getOrNull()); + } + + @Test + public void rebindingTheSameInstanceIsAccepted() { + IrisPlatform platform = new StubPlatform(); + + IrisPlatforms.bind(platform); + IrisPlatforms.bind(platform); + + assertSame(platform, IrisPlatforms.get()); + } + + @Test + public void aSecondPlatformCannotDisplaceTheBoundOne() { + IrisPlatform first = new StubPlatform(); + IrisPlatforms.bind(first); + + IllegalStateException failure = assertThrows( + IllegalStateException.class, () -> IrisPlatforms.bind(new StubPlatform())); + + assertTrue(failure.getMessage(), failure.getMessage().contains("already bound")); + assertSame(first, IrisPlatforms.get()); + } + + @Test + public void unbindIsIdempotentAndClearsTheBinding() { + IrisPlatforms.bind(new StubPlatform()); + + IrisPlatforms.unbind(); + IrisPlatforms.unbind(); + + assertFalse(IrisPlatforms.isBound()); + assertNull(IrisPlatforms.getOrNull()); + } + + @Test + public void unbindingClearsTheOnceKeysSoLaterRunsWarnAgain() { + IrisPlatforms.bind(new StubPlatform()); + assertTrue(IrisLogging.warnOnce("spi-contract-once", "first")); + assertFalse(IrisLogging.warnOnce("spi-contract-once", "second")); + + IrisPlatforms.unbind(); + + assertTrue(IrisLogging.warnOnce("spi-contract-once", "third")); + } + + private static final class StubPlatform implements IrisPlatform { + @Override + public String platformName() { + return "stub"; + } + + @Override + public String minecraftVersion() { + return "26.2"; + } + + @Override + public PlatformRegistries registries() { + throw new UnsupportedOperationException(); + } + + @Override + public PlatformScheduler scheduler() { + throw new UnsupportedOperationException(); + } + + @Override + public PlatformStructureHooks structureHooks() { + throw new UnsupportedOperationException(); + } + + @Override + public PlatformBiomeWriter biomeWriter() { + throw new UnsupportedOperationException(); + } + + @Override + public File dataFolder() { + throw new UnsupportedOperationException(); + } + + @Override + public File dataFile(String... path) { + throw new UnsupportedOperationException(); + } + + @Override + public File pluginJar() { + throw new UnsupportedOperationException(); + } + + @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) { + } + } +} diff --git a/spi/src/test/java/art/arcane/iris/spi/IrisServicesRegistryContractTest.java b/spi/src/test/java/art/arcane/iris/spi/IrisServicesRegistryContractTest.java new file mode 100644 index 000000000..eea4b67b1 --- /dev/null +++ b/spi/src/test/java/art/arcane/iris/spi/IrisServicesRegistryContractTest.java @@ -0,0 +1,93 @@ +package art.arcane.iris.spi; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +public class IrisServicesRegistryContractTest { + @Before + public void requireEmptyRegistry() { + IrisServices.clear(); + } + + @After + public void releaseRegistry() { + IrisServices.clear(); + } + + @Test + public void registeredServicesResolveThroughBothAccessors() { + Greeter greeter = new Greeter(); + + IrisServices.register(Greeter.class, greeter); + + assertSame(greeter, IrisServices.get(Greeter.class)); + assertSame(greeter, IrisServices.getOrNull(Greeter.class)); + } + + @Test + public void anAbsentServiceNamesItselfInTheFailure() { + assertNull(IrisServices.getOrNull(Greeter.class)); + + IllegalStateException failure = assertThrows(IllegalStateException.class, () -> IrisServices.get(Greeter.class)); + + assertTrue(failure.getMessage(), failure.getMessage().contains(Greeter.class.getName())); + } + + @Test + public void reregisteringATypeReplacesThePreviousImplementation() { + Greeter first = new Greeter(); + Greeter second = new Greeter(); + + IrisServices.register(Greeter.class, first); + IrisServices.register(Greeter.class, second); + + assertSame(second, IrisServices.get(Greeter.class)); + } + + @Test + public void removingAServiceLeavesTheRegistryUsable() { + IrisServices.register(Greeter.class, new Greeter()); + + IrisServices.remove(Greeter.class); + IrisServices.remove(Greeter.class); + + assertNull(IrisServices.getOrNull(Greeter.class)); + } + + @Test + public void clearDropsEveryRegistration() { + IrisServices.register(Greeter.class, new Greeter()); + IrisServices.register(Counter.class, new Counter()); + + IrisServices.clear(); + + assertNull(IrisServices.getOrNull(Greeter.class)); + assertNull(IrisServices.getOrNull(Counter.class)); + } + + @Test + public void aMismatchedImplementationIsRejectedBeforeItEntersTheRegistry() { + assertThrows(ClassCastException.class, () -> IrisServices.register(Greeter.class, new Counter())); + + assertNull(IrisServices.getOrNull(Greeter.class)); + } + + @Test + public void aNullImplementationIsRejectedBeforeItEntersTheRegistry() { + assertThrows(NullPointerException.class, () -> IrisServices.register(Greeter.class, null)); + + assertNull(IrisServices.getOrNull(Greeter.class)); + } + + private static final class Greeter { + } + + private static final class Counter { + } +} diff --git a/spi/src/test/java/art/arcane/iris/spi/PlatformValueContractTest.java b/spi/src/test/java/art/arcane/iris/spi/PlatformValueContractTest.java new file mode 100644 index 000000000..52e24a3a0 --- /dev/null +++ b/spi/src/test/java/art/arcane/iris/spi/PlatformValueContractTest.java @@ -0,0 +1,177 @@ +package art.arcane.iris.spi; + +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.assertThrows; +import static org.junit.Assert.assertTrue; + +public class PlatformValueContractTest { + private static final String FINGERPRINT = "a".repeat(64); + + @Test + public void jigsawMetadataAcceptsTheWholeDocumentedRange() { + PlatformStructureHooks.JigsawSourceMetadata smallest = + new PlatformStructureHooks.JigsawSourceMetadata(1, 0, 0); + PlatformStructureHooks.JigsawSourceMetadata largest = + new PlatformStructureHooks.JigsawSourceMetadata(128, 7, 96); + + assertEquals(1, smallest.maxDistanceHorizontal()); + assertEquals(128, largest.maxDistanceHorizontal()); + assertEquals(7, largest.referenceExpansion()); + assertEquals(96, largest.maxStartElementHorizontalSpan()); + } + + @Test + public void jigsawMetadataDefaultsTheStartElementSpanToZero() { + PlatformStructureHooks.JigsawSourceMetadata metadata = + new PlatformStructureHooks.JigsawSourceMetadata(64, 3); + + assertEquals(0, metadata.maxStartElementHorizontalSpan()); + } + + @Test + public void jigsawMetadataRejectsOutOfRangeGeometry() { + assertThrows(IllegalArgumentException.class, + () -> new PlatformStructureHooks.JigsawSourceMetadata(0, 0, 0)); + assertThrows(IllegalArgumentException.class, + () -> new PlatformStructureHooks.JigsawSourceMetadata(129, 0, 0)); + assertThrows(IllegalArgumentException.class, + () -> new PlatformStructureHooks.JigsawSourceMetadata(64, -1, 0)); + assertThrows(IllegalArgumentException.class, + () -> new PlatformStructureHooks.JigsawSourceMetadata(64, 0, -1)); + } + + @Test + public void unimplementedStructureHookDefaultsRefuseInsteadOfReturningNothing() { + PlatformStructureHooks hooks = new MinimalStructureHooks(); + + assertEquals(List.of(), hooks.jigsawStructureKeys()); + assertEquals(List.of(), hooks.templatePoolKeys()); + assertThrows(UnsupportedOperationException.class, () -> hooks.jigsawSourceMetadata("iris:village")); + assertThrows(UnsupportedOperationException.class, () -> hooks.templatePoolHorizontalSpan("iris:pool")); + assertThrows(UnsupportedOperationException.class, + () -> hooks.jigsawStartPoolHorizontalSpan("iris:village", "iris:pool")); + } + + @Test + public void definitionFactoriesCarryTheirEncoding() { + PlatformGenerationRegistry.Definition json = PlatformGenerationRegistry.Definition.exactJson("{}"); + PlatformGenerationRegistry.Definition identity = + PlatformGenerationRegistry.Definition.conservativeIdentity("abc"); + + assertEquals(PlatformGenerationRegistry.Encoding.JSON, json.encoding()); + assertEquals("{}", json.value()); + assertEquals(PlatformGenerationRegistry.Encoding.CONSERVATIVE_IDENTITY, identity.encoding()); + assertEquals("abc", identity.value()); + } + + @Test + public void resourceIdentityTrimsItsPartsIntoAStableKey() { + PlatformGenerationRegistry.Definition definition = + PlatformGenerationRegistry.Definition.resourceIdentity(" worldgen/biome ", " iris:plains "); + + assertEquals(PlatformGenerationRegistry.Encoding.CONSERVATIVE_IDENTITY, definition.encoding()); + assertEquals("registry-resource-key-v1|worldgen/biome|iris:plains", definition.value()); + } + + @Test + public void definitionsRejectMissingAndBlankValues() { + assertThrows(NullPointerException.class, + () -> new PlatformGenerationRegistry.Definition(null, "value")); + assertThrows(NullPointerException.class, + () -> new PlatformGenerationRegistry.Definition(PlatformGenerationRegistry.Encoding.JSON, null)); + assertThrows(IllegalArgumentException.class, + () -> new PlatformGenerationRegistry.Definition(PlatformGenerationRegistry.Encoding.JSON, " ")); + assertThrows(IllegalArgumentException.class, + () -> PlatformGenerationRegistry.Definition.resourceIdentity(" ", "iris:plains")); + assertThrows(IllegalArgumentException.class, + () -> PlatformGenerationRegistry.Definition.resourceIdentity("worldgen/biome", " ")); + } + + @Test + public void customBiomeResourceKeysAreContentAddressed() { + assertEquals("iris:biomes/" + FINGERPRINT, + PlatformGenerationRegistry.contentAddressedCustomBiomeResourceKey(FINGERPRINT)); + } + + @Test + public void customBiomeResourceKeysRejectAnythingButALowercaseSha256() { + assertThrows(NullPointerException.class, + () -> PlatformGenerationRegistry.contentAddressedCustomBiomeResourceKey(null)); + assertThrows(IllegalArgumentException.class, + () -> PlatformGenerationRegistry.contentAddressedCustomBiomeResourceKey(FINGERPRINT.toUpperCase())); + assertThrows(IllegalArgumentException.class, + () -> PlatformGenerationRegistry.contentAddressedCustomBiomeResourceKey("a".repeat(63))); + assertThrows(IllegalArgumentException.class, + () -> PlatformGenerationRegistry.contentAddressedCustomBiomeResourceKey("z".repeat(64))); + } + + @Test + public void blockPropertiesReportWhetherTheyCarryANumericRange() { + PlatformNumericRange range = new PlatformNumericRange(0D, 15D, false, true); + PlatformBlockProperty numeric = + new PlatformBlockProperty("level", "integer", 0, List.of(0, 15), range); + PlatformBlockProperty enumerated = + new PlatformBlockProperty("axis", "string", "y", List.of("x", "y", "z"), null); + + assertTrue(numeric.hasNumericRange()); + assertEquals(range, numeric.numericRange()); + assertEquals(0D, range.minimum(), 0D); + assertEquals(15D, range.maximum(), 0D); + assertFalse(range.exclusiveMinimum()); + assertTrue(range.exclusiveMaximum()); + assertFalse(enumerated.hasNumericRange()); + assertEquals(List.of("x", "y", "z"), enumerated.allowedValues()); + } + + private static final class MinimalStructureHooks implements PlatformStructureHooks { + @Override + public List structureKeys() { + return List.of(); + } + + @Override + public List structureSetKeys() { + return List.of(); + } + + @Override + public List structureBiomeKeys(String structureKey) { + return List.of(); + } + + @Override + public List objectFeatureKeys() { + return List.of(); + } + + @Override + public List reachableStructureKeys(PlatformWorld world) { + return List.of(); + } + + @Override + public List possibleBiomeKeys(PlatformWorld world) { + return List.of(); + } + + @Override + public boolean placeFeature(PlatformWorld world, int x, int y, int z, String featureKey, long seed) { + return false; + } + + @Override + public int[] placeStructure(PlatformWorld world, int chunkX, int chunkZ, String structureKey, long seed, int maxSpan) { + return new int[0]; + } + + @Override + public boolean supportsStructurePlacement() { + return false; + } + } +}