From ff380ee3bc9d48f4e0d2f75683be7ccb48274d3a Mon Sep 17 00:00:00 2001 From: Algent Date: Sat, 15 Aug 2026 10:31:59 +0200 Subject: [PATCH] test: characterize MultiPartRegistry Splits the characterization across both layers, because part registration needs FML's active mod container and throws without one, so it cannot run headless at all. Plain JVM covers what is reachable: converters are dispatched per block, a converter returning null does not stop the scan, converters registered for another block are never consulted, a converter claiming several blocks is registered for each, and an unknown mod container throws rather than returning null. The Forge server suite covers registration itself, and deliberately exercises both overloads downstream depends on. Content registers through the deprecated IPartFactory array form that seven jars use, and each microblock class registers through the IPartFactory2 varargs form that ProjRed uses, so resolving mc_torch and mcr_face proves both paths. Part ids also round trip through the shared IDWriter carrier. 78 plain-JVM tests and 5 server tests, all passing against the untouched Scala. --- .../test/ForgeEnvironmentSmokeTest.java | 68 +++++++++++ ...MultiPartRegistryCharacterizationTest.java | 113 ++++++++++++++++++ 2 files changed, 181 insertions(+) create mode 100644 src/test/java/codechicken/multipart/MultiPartRegistryCharacterizationTest.java diff --git a/src/functionalTest/java/codechicken/multipart/test/ForgeEnvironmentSmokeTest.java b/src/functionalTest/java/codechicken/multipart/test/ForgeEnvironmentSmokeTest.java index 2c50b3e..08487b6 100644 --- a/src/functionalTest/java/codechicken/multipart/test/ForgeEnvironmentSmokeTest.java +++ b/src/functionalTest/java/codechicken/multipart/test/ForgeEnvironmentSmokeTest.java @@ -7,17 +7,28 @@ import static org.junit.jupiter.api.Assertions.assertNotSame; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.lang.reflect.Proxy; import java.util.BitSet; +import net.minecraft.nbt.NBTTagCompound; import net.minecraft.server.MinecraftServer; import org.junit.jupiter.api.Test; +import codechicken.lib.data.MCDataInput; +import codechicken.lib.data.MCDataOutput; +import codechicken.lib.data.MCDataOutputWrapper; import codechicken.microblock.FaceMicroClass$; import codechicken.microblock.HollowMicroClass$; import codechicken.microblock.Microblock; import codechicken.microblock.MicroblockGenerator; +import codechicken.multipart.MultiPartRegistry; import codechicken.multipart.TFacePart; +import codechicken.multipart.TMultiPart; import codechicken.multipart.TileMultipart; import codechicken.multipart.asm.MultipartMixinFactory; import codechicken.multipart.scalatraits.TSlottedTile; @@ -65,6 +76,63 @@ class ForgeEnvironmentSmokeTest { assertFalse(((TFacePart) hollow).solid(0)); } + /** + * Part registration needs FML's active mod container, so none of this is reachable from a plain JVM test. Startup + * exercises both overloads downstream depends on: Content registers through the deprecated IPartFactory array form, + * and each microblock class registers through the IPartFactory2 varargs form. + */ + @Test + void registryResolvesPartsRegisteredThroughBothFactoryOverloads() throws Exception { + assertTrue(MultiPartRegistry.loaded(), "Registration must be closed once the server has started"); + + // mc_torch comes from the deprecated IPartFactory array overload. + TMultiPart torch = MultiPartRegistry.loadPart("mc_torch", null); + assertNotNull(torch); + assertEquals("mc_torch", torch.getType()); + + // mcr_face comes from the IPartFactory2 varargs overload. Its factory reads the tag, so it needs a real one; + // an absent material name resolves to the missing-material placeholder rather than failing. + TMultiPart face = MultiPartRegistry.loadPart("mcr_face", new NBTTagCompound()); + assertNotNull(face); + assertEquals("mcr_face", face.getType()); + + assertNotNull(MultiPartRegistry.getModContainer("mc_torch")); + assertNotNull(MultiPartRegistry.getModContainer("mcr_face")); + } + + @Test + void partIdsRoundTripThroughTheRegistryCarrier() { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + MCDataOutput output = new MCDataOutputWrapper(new DataOutputStream(bytes)); + TMultiPart torch = MultiPartRegistry.loadPart("mc_torch", null); + MultiPartRegistry.writePartID(output, torch); + + TMultiPart read = MultiPartRegistry.readPart(input(bytes.toByteArray())); + + assertNotNull(read); + assertEquals("mc_torch", read.getType()); + } + + /** CodeChickenLib has no stream-backed MCDataInput, so the carrier reads are proxied. */ + private static MCDataInput input(byte[] bytes) { + DataInputStream data = new DataInputStream(new ByteArrayInputStream(bytes)); + return (MCDataInput) Proxy.newProxyInstance( + MCDataInput.class.getClassLoader(), + new Class[] { MCDataInput.class }, + (proxy, method, arguments) -> { + switch (method.getName()) { + case "readUByte": + return (short) data.readUnsignedByte(); + case "readUShort": + return data.readUnsignedShort(); + case "readInt": + return data.readInt(); + default: + throw new AssertionError("Unexpected read method: " + method.getName()); + } + }); + } + @Test void generatesAndCachesSlottedTileClass() { int traitId = MultipartMixinFactory.getId(TSlottedTile.class.getName().replace('.', '/')); diff --git a/src/test/java/codechicken/multipart/MultiPartRegistryCharacterizationTest.java b/src/test/java/codechicken/multipart/MultiPartRegistryCharacterizationTest.java new file mode 100644 index 0000000..89cea3b --- /dev/null +++ b/src/test/java/codechicken/multipart/MultiPartRegistryCharacterizationTest.java @@ -0,0 +1,113 @@ +package codechicken.multipart; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.NoSuchElementException; + +import net.minecraft.block.Block; +import net.minecraft.block.material.Material; +import net.minecraft.world.World; + +import org.junit.jupiter.api.Test; + +import codechicken.lib.vec.BlockCoord; +import codechicken.multipart.MultiPartRegistry.IPartConverter; + +/** + * Part registration needs FML's active mod container, so it cannot run headless; that half of the registry is + * characterized in the Forge server suite instead. Converter registration and dispatch touch neither FML nor the + * logger, so they are covered here. + */ +class MultiPartRegistryCharacterizationTest { + + @Test + void noPartsAreLoadedBeforeRegistrationRuns() { + assertFalse(MultiPartRegistry.loaded()); + } + + @Test + void unknownModContainerThrowsRatherThanReturningNull() { + assertThrows(NoSuchElementException.class, () -> MultiPartRegistry.getModContainer("test:nosuchpart")); + } + + @Test + void convertersAreDispatchedPerBlockAndFirstNonNullWins() { + Block block = new Block(Material.rock) {}; + Block other = new Block(Material.rock) {}; + + RecordingConverter declines = new RecordingConverter(block, null); + RecordingConverter accepts = new RecordingConverter(block, new TestPart()); + RecordingConverter unrelated = new RecordingConverter(other, new TestPart()); + + MultiPartRegistry.registerConverter(declines); + MultiPartRegistry.registerConverter(accepts); + MultiPartRegistry.registerConverter(unrelated); + + assertSame(accepts.result, MultiPartRegistry.convertBlock(null, null, block)); + assertEquals(1, declines.calls, "A converter returning null must not stop the scan"); + assertEquals(1, accepts.calls); + assertEquals(0, unrelated.calls, "Converters registered for another block must not be consulted"); + } + + @Test + void blockWithNoConverterYieldsNull() { + assertNull(MultiPartRegistry.convertBlock(null, null, new Block(Material.rock) {})); + } + + @Test + void aConverterIsRegisteredForEveryBlockItClaims() { + Block first = new Block(Material.rock) {}; + Block second = new Block(Material.rock) {}; + TestPart part = new TestPart(); + + RecordingConverter converter = new RecordingConverter(Arrays.asList(first, second), part); + MultiPartRegistry.registerConverter(converter); + + assertSame(part, MultiPartRegistry.convertBlock(null, null, first)); + assertSame(part, MultiPartRegistry.convertBlock(null, null, second)); + assertEquals(2, converter.calls); + } + + private static final class RecordingConverter implements IPartConverter { + + private final List blocks = new ArrayList<>(); + private final TMultiPart result; + private int calls; + + private RecordingConverter(Block block, TMultiPart result) { + this.blocks.add(block); + this.result = result; + } + + private RecordingConverter(List blocks, TMultiPart result) { + this.blocks.addAll(blocks); + this.result = result; + } + + @Override + public Iterable blockTypes() { + return blocks; + } + + @Override + public TMultiPart convert(World world, BlockCoord pos) { + calls++; + return result; + } + } + + private static final class TestPart extends TMultiPart { + + @Override + public String getType() { + return "test:converted"; + } + } +}