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.
This commit is contained in:
Algent committed 2026-08-15 10:31:59 +02:00
1 parent 9f454042b4
commit ff380ee3bc
2 files changed
+181

No files matched your search

@@ -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('.', '/'));
@@ -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<Block> 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<Block> blocks, TMultiPart result) {
this.blocks.addAll(blocks);
this.result = result;
}
@Override
public Iterable<Block> 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";
}
}
}