test: characterize MicroMaterialRegistry

Freezes the registry lifecycle before the port: setupIDMap sorts the type
map by name into the id array, ids are the index into it, materialID and
materialName round trip, remapName resolves on both getMaterial(String)
and materialID, getMissingId points at the registered placeholder rather
than falling back to zero, ids round trip through the shared IDWriter, and
a highlight renderer that claims the highlight short-circuits the default
client renderer.

Also freezes the two members IMicroMaterial supplies. A Java class cannot
inherit them today, so they are observed through MissingMicroMaterial,
which overrides only isTransparent and loadIcons.

Two limits are deliberate and recorded in the test. The registry is global
mutable state, so both test classes guard their registrations and assert
only relative facts. The error paths are not covered at all: they call the
microblock logger, which is null until MicroblockProxy.preInit runs.

Adds a frozen Scala 2.11.5 consumer compiled against the reference dev jar
at 52ff8ff, reading MODULE$ and the raw scala.Tuple2 array.

73 plain-JVM tests, all passing against the untouched Scala.
This commit is contained in:
Algent committed 2026-08-14 22:03:04 +02:00
1 parent 52ff8ff460
commit dc758d4310
5 files changed
+392

No files matched your search

+12
View File
@@ -112,3 +112,15 @@ Compiled with Scala 2.11.5 under Java 8, `-target:jvm-1.8`, against the referenc
(SHA-256 `e931927e83134f57197aba082345ea96909291469d01a4f146836418a76bb3d1`). Class-file SHA-256 is
`a03e8eb8566ae830829d59a379c67f355519bc82de24265baabd84720f068ce1`, stored as
`src/test/resources/compat/ReferenceScalaSaw.class.b64`.
### `ReferenceScalaMicroMaterialConsumer`
`scala/codechicken/multipart/compat/ReferenceScalaMicroMaterialConsumer.scala` reads
`MicroMaterialRegistry$.MODULE$` and calls the instance methods ProjRed links against (`getMaterial(int)`,
`materialID`, `materialName`), plus the `scala.Tuple2` array `getIdMap` that extrautilities links against. It reads
`_1` and `_2` off the raw tuples rather than going through accessors, so the array element type is exercised too.
Compiled with Scala 2.11.5 under Java 8, `-target:jvm-1.8`, against the reference dev jar built at `52ff8ff`
(SHA-256 `122d89cf0a070cfd2325bbbcf4bb4550d6130bb7da1ffba1a3b2e2efea6ec7e5`). Class-file SHA-256 is
`e66be14e6422277cc9567acf8e96a26e419abf5ec6582085c8c6e32cc60aa599`, stored as
`src/test/resources/compat/ReferenceScalaMicroMaterialConsumer.class.b64`.
@@ -0,0 +1,21 @@
package codechicken.multipart.compat
import codechicken.microblock.MicroMaterialRegistry
import codechicken.microblock.MicroMaterialRegistry.IMicroMaterial
/** Compiled against the reference dev jar. Reads MicroMaterialRegistry$.MODULE$ and calls the instance methods ProjRed
* links against, plus the Tuple2-array getIdMap that extrautilities links against.
*/
class ReferenceScalaMicroMaterialConsumer {
def materialAt(id: Int): IMicroMaterial = MicroMaterialRegistry.getMaterial(id)
def idOf(name: String): Int = MicroMaterialRegistry.materialID(name)
def nameOf(id: Int): String = MicroMaterialRegistry.materialName(id)
/** Exercises the raw scala.Tuple2 array descriptor rather than the accessors. */
def idMapEntry(id: Int): String = {
val entry = MicroMaterialRegistry.getIdMap.apply(id)
entry._1 + "=" + (entry._2 eq materialAt(id))
}
}
@@ -0,0 +1,75 @@
package codechicken.microblock;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Objects;
import java.util.Scanner;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import codechicken.microblock.MicroMaterialRegistry.IMicroMaterial;
/**
* Loads a Scala class compiled against the reference dev jar. It reads MicroMaterialRegistry$.MODULE$ and calls the
* instance methods ProjRed links against, plus the scala.Tuple2 array getIdMap that extrautilities links against.
*
* Registration is global, so this registers its own uniquely named material and only asserts facts about it.
*/
class MicroMaterialRegistryBinaryCompatibilityTest {
private static final String NAME = "test:binarycompat";
@BeforeAll
static void register() {
// The other test class may already have registered the placeholder; re-registering would hit the error path.
if (MicroMaterialRegistry.getMaterial(MissingMicroMaterial.key()) == null) {
MicroMaterialRegistry.registerMaterial(MissingMicroMaterial$.MODULE$, MissingMicroMaterial.key());
}
if (MicroMaterialRegistry.getMaterial(NAME) == null) {
MicroMaterialRegistry.registerMaterial(MissingMicroMaterial$.MODULE$, NAME);
}
MicroMaterialRegistry$.MODULE$.setupIDMap();
}
@Test
void referenceScalaConsumerStillLinksAgainstTheSingletonAndIdMap() throws Exception {
Class<?> fixtureClass = new FixtureClassLoader(MicroMaterialRegistry.class.getClassLoader())
.define(loadFixture());
Object fixture = fixtureClass.getDeclaredConstructor().newInstance();
int id = (Integer) fixtureClass.getMethod("idOf", String.class).invoke(fixture, NAME);
assertEquals(NAME, fixtureClass.getMethod("nameOf", int.class).invoke(fixture, id));
Object material = fixtureClass.getMethod("materialAt", int.class).invoke(fixture, id);
assertSame(MicroMaterialRegistry.getMaterial(id), material);
assertSame(MicroMaterialRegistry.getMaterial(id), (IMicroMaterial) material);
// Reads the raw scala.Tuple2 array rather than the accessors.
assertEquals(NAME + "=true", fixtureClass.getMethod("idMapEntry", int.class).invoke(fixture, id));
}
private static byte[] loadFixture() {
InputStream input = Objects.requireNonNull(
MicroMaterialRegistryBinaryCompatibilityTest.class
.getResourceAsStream("/compat/ReferenceScalaMicroMaterialConsumer.class.b64"));
try (Scanner scanner = new Scanner(input, StandardCharsets.US_ASCII.name()).useDelimiter("\\A")) {
return Base64.getMimeDecoder().decode(scanner.next());
}
}
private static final class FixtureClassLoader extends ClassLoader {
private FixtureClassLoader(ClassLoader parent) {
super(parent);
}
private Class<?> define(byte[] bytecode) {
return defineClass(null, bytecode, 0, bytecode.length);
}
}
}
@@ -0,0 +1,242 @@
package codechicken.microblock;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
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.assertTrue;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.lang.reflect.Proxy;
import net.minecraft.block.Block.SoundType;
import net.minecraft.entity.Entity;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.item.ItemStack;
import net.minecraft.util.IIcon;
import net.minecraft.util.MovingObjectPosition;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import codechicken.lib.data.MCDataInput;
import codechicken.lib.data.MCDataOutput;
import codechicken.lib.data.MCDataOutputWrapper;
import codechicken.lib.vec.Cuboid6;
import codechicken.lib.vec.Vector3;
import codechicken.microblock.MicroMaterialRegistry.IMicroHighlightRenderer;
import codechicken.microblock.MicroMaterialRegistry.IMicroMaterial;
/**
* The registry is global mutable state, so everything is registered once in {@link #registerAndFreezeIdMap()} and the
* cases below only read it. Error paths are not covered: they call the microblock logger, which is null until
* MicroblockProxy.preInit runs, so they cannot execute headless.
*/
class MicroMaterialRegistryCharacterizationTest {
private static final TestMaterial STONE = new TestMaterial(false, 3);
private static final TestMaterial GLASS = new TestMaterial(true, 1);
@BeforeAll
static void registerAndFreezeIdMap() {
// Registration is global and re-registering hits the logger-backed error path, which cannot run headless.
if (MicroMaterialRegistry.getMaterial(MissingMicroMaterial.key()) == null) {
MicroMaterialRegistry.registerMaterial(MissingMicroMaterial$.MODULE$, MissingMicroMaterial.key());
}
MicroMaterialRegistry.registerMaterial(STONE, "test:stone");
MicroMaterialRegistry.registerMaterial(GLASS, "test:glass");
MicroMaterialRegistry.remapName("test:oldglass", "test:glass");
// setupIDMap is package private in Scala, so it has no static forwarder and must go through the singleton.
MicroMaterialRegistry$.MODULE$.setupIDMap();
}
@Test
void idMapIsSortedByNameAndDrivesBothLookups() {
String[] names = new String[MicroMaterialRegistry.getIdMap().length];
for (int i = 0; i < names.length; i++) {
names[i] = MicroMaterialRegistry.getIdMap()[i]._1();
}
String[] sorted = names.clone();
java.util.Arrays.sort(sorted);
assertArrayEquals(sorted, names);
for (int i = 0; i < names.length; i++) {
assertEquals(i, MicroMaterialRegistry.materialID(names[i]));
assertEquals(names[i], MicroMaterialRegistry.materialName(i));
assertSame(MicroMaterialRegistry.getIdMap()[i]._2(), MicroMaterialRegistry.getMaterial(i));
}
}
@Test
void getMaterialByNameResolvesRemapsAndReturnsNullWhenUnknown() {
assertSame(GLASS, MicroMaterialRegistry.getMaterial("test:glass"));
assertSame(GLASS, MicroMaterialRegistry.getMaterial("test:oldglass"));
assertNull(MicroMaterialRegistry.getMaterial("test:nosuchmaterial"));
}
@Test
void materialIdResolvesRemappedNames() {
assertEquals(MicroMaterialRegistry.materialID("test:glass"), MicroMaterialRegistry.materialID("test:oldglass"));
}
@Test
void missingIdPointsAtTheMissingMaterialRatherThanZero() {
assertEquals(
MicroMaterialRegistry.materialID(MissingMicroMaterial.key()),
MicroMaterialRegistry.getMissingId());
assertSame(
MissingMicroMaterial$.MODULE$,
MicroMaterialRegistry.getMaterial(MicroMaterialRegistry.getMissingId()));
}
@Test
void materialIdsRoundTripThroughTheIdWriter() throws Exception {
for (int id = 0; id < MicroMaterialRegistry.getIdMap().length; id++) {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
MCDataOutput output = new MCDataOutputWrapper(new DataOutputStream(bytes));
MicroMaterialRegistry.writeMaterialID(output, id);
byte[] encoded = bytes.toByteArray();
assertEquals(1, encoded.length, "A small id map must still use the byte carrier");
assertEquals(id, MicroMaterialRegistry.readMaterialID(input(encoded)));
}
}
@Test
void highlightRendererShortCircuitsTheDefaultRenderer() {
RecordingRenderer accepting = new RecordingRenderer(true);
MicroMaterialRegistry.registerHighlightRenderer(accepting);
// A renderer that claims the highlight stops the default client renderer being reached at all.
assertTrue(MicroMaterialRegistry.renderHighlight(null, null, null, 2, 0));
assertEquals(1, accepting.calls);
}
/**
* MissingMicroMaterial overrides only isTransparent and loadIcons, so it inherits canRenderInPass and isSolid from
* the trait. A Java class cannot inherit them today, so this is the only way to observe the supplied values.
*/
@Test
void materialInterfaceSuppliesRenderPassAndSolidity() {
assertTrue(MissingMicroMaterial.canRenderInPass(0));
assertFalse(MissingMicroMaterial.canRenderInPass(1));
assertFalse(MissingMicroMaterial.canRenderInPass(-1));
assertFalse(MissingMicroMaterial.isTransparent());
assertTrue(MissingMicroMaterial.isSolid());
}
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());
}
});
}
private static final class RecordingRenderer implements IMicroHighlightRenderer {
private final boolean handled;
private int calls;
private RecordingRenderer(boolean handled) {
this.handled = handled;
}
@Override
public boolean renderHighlight(EntityPlayer player, MovingObjectPosition hit, CommonMicroClass mcrClass,
int size, int material) {
calls++;
return handled;
}
}
/** Mirrors the forwarders Scala emits, so this file compiles against the trait and against the Java interface. */
private static final class TestMaterial implements IMicroMaterial {
private final boolean transparent;
private final int cutterStrength;
private TestMaterial(boolean transparent, int cutterStrength) {
this.transparent = transparent;
this.cutterStrength = cutterStrength;
}
@Override
public IIcon getBreakingIcon(int side) {
return null;
}
@Override
public void loadIcons() {}
@Override
public void renderMicroFace(Vector3 pos, int pass, Cuboid6 bounds) {}
@Override
public boolean canRenderInPass(int pass) {
return pass == 0;
}
@Override
public boolean isTransparent() {
return transparent;
}
@Override
public int getLightValue() {
return 0;
}
@Override
public float getStrength(EntityPlayer player) {
return 1f;
}
@Override
public String getLocalizedName() {
return "Test Material";
}
@Override
public ItemStack getItem() {
return null;
}
@Override
public int getCutterStrength() {
return cutterStrength;
}
@Override
public SoundType getSound() {
return null;
}
@Override
public boolean isSolid() {
return !isTransparent();
}
@Override
public float explosionResistance(Entity entity) {
return 1f;
}
}
}
@@ -0,0 +1,42 @@
yv66vgAAADQAXAEAQGNvZGVjaGlja2VuL211bHRpcGFydC9jb21wYXQvUmVmZXJlbmNlU2NhbGFN
aWNyb01hdGVyaWFsQ29uc3VtZXIHAAEBABBqYXZhL2xhbmcvT2JqZWN0BwADAQApUmVmZXJlbmNl
U2NhbGFNaWNyb01hdGVyaWFsQ29uc3VtZXIuc2NhbGEBAB5Mc2NhbGEvcmVmbGVjdC9TY2FsYVNp
Z25hdHVyZTsBAAVieXRlcwEB5AYBBTNBIQECARMJGSMrGjRmZRZ0Ny1aKmRDMgxXKlsyc182C0cv
GjpqQzIcdU5cOnZbFhQoQkECBQMZGXcuHDliaSoRUUFCAQpbVmRHLxs5YmVSVBFhQgEMRz4kV20Z
NWpHLixnbgEBFAUBUQFDQQYPGwVhISJBBwILTRwXDVwxCgU9YSFBQiFvc0orZw1DAxIBEQUhIwEE
PVM6TEdPEAsCJ0ERQQNBBwIFISlhAwFDAS8FUVEuGTtmZSYMRy4ROxUFYUEDQ0ENJh0JUSJFBAIc
QTkRQWRIBwI7KRFhBEMBB3lJfHcOHiAKAx1JISEJBAIVNUw3TV04Y1k+HDcuAwIkSQUpUipbMnNf
NgtHLxo6akMyFFZtWjV0aUpMKEJBEQcTCTFzRQEIShsmHCdvXCdiaRYUGC4ZNwsFDSIDIkIVFgEE
URNBQTVlIQlZMSYDAi0ZCRkRShw7CQs5AkERQRgCCSUkd0oaCwNVQUJRIU0XQQJJCkFBXDFuS0IR
MUcOCAMXUUohIQ4HAg1BE1gNWjNnEwk5BEgBBFRpSkxnbhoGA2sxQVFBDwEFAm0KYUFcMW5LPjNH
QwEaPREVSRMIMQErERVxBAEiAUADKUlHLVQxcQs6JCgvHwsDZQFDUSFLH0ECKQIBAAptYXRlcmlh
bEF0AQBAKEkpTGNvZGVjaGlja2VuL21pY3JvYmxvY2svTWljcm9NYXRlcmlhbFJlZ2lzdHJ5JElN
aWNyb01hdGVyaWFsOwEALWNvZGVjaGlja2VuL21pY3JvYmxvY2svTWljcm9NYXRlcmlhbFJlZ2lz
dHJ5JAcACwEAB01PRFVMRSQBAC9MY29kZWNoaWNrZW4vbWljcm9ibG9jay9NaWNyb01hdGVyaWFs
UmVnaXN0cnkkOwwADQAOCQAMAA8BAAtnZXRNYXRlcmlhbAwAEQAKCgAMABIBAAR0aGlzAQBCTGNv
ZGVjaGlja2VuL211bHRpcGFydC9jb21wYXQvUmVmZXJlbmNlU2NhbGFNaWNyb01hdGVyaWFsQ29u
c3VtZXI7AQACaWQBAAFJAQAEaWRPZgEAFShMamF2YS9sYW5nL1N0cmluZzspSQEACm1hdGVyaWFs
SUQMABoAGQoADAAbAQAEbmFtZQEAEkxqYXZhL2xhbmcvU3RyaW5nOwEABm5hbWVPZgEAFShJKUxq
YXZhL2xhbmcvU3RyaW5nOwEADG1hdGVyaWFsTmFtZQwAIQAgCgAMACIBAAppZE1hcEVudHJ5AQAI
Z2V0SWRNYXABABEoKVtMc2NhbGEvVHVwbGUyOwwAJQAmCgAMACcBACZzY2FsYS9jb2xsZWN0aW9u
L211dGFibGUvU3RyaW5nQnVpbGRlcgcAKQEABjxpbml0PgEAAygpVgwAKwAsCgAqAC0BAAxzY2Fs
YS9UdXBsZTIHAC8BAAJfMQEAFCgpTGphdmEvbGFuZy9PYmplY3Q7DAAxADIKADAAMwEAEGphdmEv
bGFuZy9TdHJpbmcHADUBAAZhcHBlbmQBADwoTGphdmEvbGFuZy9PYmplY3Q7KUxzY2FsYS9jb2xs
ZWN0aW9uL211dGFibGUvU3RyaW5nQnVpbGRlcjsMADcAOAoAKgA5AQABPQgAOwEAAl8yDAA9ADIK
ADAAPgwACQAKCgACAEABABpzY2FsYS9ydW50aW1lL0JveGVzUnVuVGltZQcAQgEADGJveFRvQm9v
bGVhbgEAFihaKUxqYXZhL2xhbmcvQm9vbGVhbjsMAEQARQoAQwBGAQAIdG9TdHJpbmcBABQoKUxq
YXZhL2xhbmcvU3RyaW5nOwwASABJCgAqAEoBAAVlbnRyeQEADkxzY2FsYS9UdXBsZTI7CgAEAC0B
ADtjb2RlY2hpY2tlbi9taWNyb2Jsb2NrL01pY3JvTWF0ZXJpYWxSZWdpc3RyeSRJTWljcm9NYXRl
cmlhbAcATwEALGNvZGVjaGlja2VuL21pY3JvYmxvY2svTWljcm9NYXRlcmlhbFJlZ2lzdHJ5BwBR
AQAOSU1pY3JvTWF0ZXJpYWwBAARDb2RlAQASTG9jYWxWYXJpYWJsZVRhYmxlAQAPTGluZU51bWJl
clRhYmxlAQANU3RhY2tNYXBUYWJsZQEAClNvdXJjZUZpbGUBAAxJbm5lckNsYXNzZXMBABlSdW50
aW1lVmlzaWJsZUFubm90YXRpb25zAQAIU2NhbGFTaWcAIQACAAQAAAAAAAUAAQAJAAoAAQBUAAAA
PAACAAIAAAAIsgAQG7YAE7AAAAACAFUAAAAWAAIAAAAIABQAFQAAAAAACAAWABcAAQBWAAAABgAB
AAAACgABABgAGQABAFQAAAA8AAIAAgAAAAiyABArtgAcrAAAAAIAVQAAABYAAgAAAAgAFAAVAAAA
AAAIAB0AHgABAFYAAAAGAAEAAAAMAAEAHwAgAAEAVAAAADwAAgACAAAACLIAEBu2ACOwAAAAAgBV
AAAAFgACAAAACAAUABUAAAAAAAgAFgAXAAEAVgAAAAYAAQAAAA4AAQAkACAAAQBUAAAApwAEAAMA
AAA6sgAQtgAoGzJNuwAqWbcALiy2ADTAADa2ADoSPLYAOiy2AD8qG7YAQaYABwSnAAQDuABHtgA6
tgBLsAAAAAMAVQAAACAAAwAAADoAFAAVAAAAAAA6ABYAFwABAAkAMABMAE0AAgBWAAAACgACAAAA
EgAJABMAVwAAACUAAv8ALwADBwACAQcAMAABBwAq/wAAAAMHAAIBBwAwAAIHACoBAAEAKwAsAAEA
VAAAAC8AAQABAAAABSq3AE6xAAAAAgBVAAAADAABAAAABQAUABUAAABWAAAABgABAAAAFQAEAFgA
AAACAAUAWQAAAAoAAQBQAFIAUwYJAFoAAAALAAEABgABAAdzAAgAWwAAAAMFAAA=