test: characterize multipart mixin factory

This commit is contained in:
Algent committed 2026-09-02 23:15:20 +02:00
1 parent 09b9ed77a6
commit f6f0eece7f
1 file changed
+179
@@ -0,0 +1,179 @@
package codechicken.multipart.asm;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Set;
import java.util.TreeSet;
import org.junit.jupiter.api.Test;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.LdcInsnNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
/**
* The companion's initializer reaches Forge through TileMultipart, so generation is exercised by the Forge suite:
* generated tiles, pass-through delegation and the copied fields of Java traits. Surface and emission order are checked
* here.
*/
class MultipartMixinFactoryCharacterizationTest {
@Test
void keepsExactFacadeSurface() throws Exception {
assertEquals(Modifier.PUBLIC | Modifier.FINAL, MultipartMixinFactory.class.getModifiers());
assertSame(Object.class, MultipartMixinFactory.class.getSuperclass());
// onCompiled, autoCompleteJavaTrait and both mangled helpers are the ledger's additive forwarders.
assertEquals(
signatures(
"baseType()Ljava/lang/Class;",
"construct(Ljava/util/BitSet;Lscala/collection/Seq;)Ljava/lang/Object;",
"getId(Ljava/lang/String;)I",
"registerTrait(Ljava/lang/Class;)I",
"registerTrait(Ljava/lang/String;)I",
"onCompiled(Ljava/lang/Class;Ljava/util/BitSet;)V",
"autoCompleteJavaTrait(Lorg/objectweb/asm/tree/ClassNode;)V",
"generatePassThroughTrait(Ljava/lang/String;)Ljava/lang/String;",
"codechicken$multipart$asm$ASMMixinFactory$$concreteParent$1"
+ "(Lcodechicken/multipart/asm/ASMMixinCompiler$ClassInfo;)"
+ "Lcodechicken/multipart/asm/ASMMixinCompiler$ClassInfo;",
"codechicken$multipart$asm$ASMMixinFactory$$checkParent$1"
+ "(Lcodechicken/multipart/asm/ASMMixinCompiler$ClassInfo;Ljava/lang/String;)Z"),
publicMethods(MultipartMixinFactory.class));
for (Method method : MultipartMixinFactory.class.getDeclaredMethods()) {
assertTrue(Modifier.isStatic(method.getModifiers()));
}
assertEquals(0, MultipartMixinFactory.class.getDeclaredFields().length);
assertTrue(Modifier.isPrivate(MultipartMixinFactory.class.getDeclaredConstructor().getModifiers()));
}
@Test
void keepsTheCompanionAsATileMultipartFactory() throws Exception {
assertEquals(Modifier.PUBLIC | Modifier.FINAL, MultipartMixinFactory$.class.getModifiers());
assertSame(ASMMixinFactory.class, MultipartMixinFactory$.class.getSuperclass());
assertEquals(
signatures(
"onCompiled(Ljava/lang/Class;Ljava/util/BitSet;)V",
"autoCompleteJavaTrait(Lorg/objectweb/asm/tree/ClassNode;)V",
"generatePassThroughTrait(Ljava/lang/String;)Ljava/lang/String;",
"codechicken$multipart$asm$MultipartMixinFactory$$methods$1"
+ "(Lorg/objectweb/asm/tree/ClassNode;)Lscala/collection/immutable/Map;",
"codechicken$multipart$asm$MultipartMixinFactory$$generatePassThroughMethod$1"
+ "(Lorg/objectweb/asm/tree/MethodNode;Ljava/lang/String;Ljava/lang/String;"
+ "Ljava/lang/String;Ljava/lang/String;Lcodechicken/lib/asm/CC_ClassWriter;"
+ "Lscala/runtime/ObjectRef;)V"),
publicMethods(MultipartMixinFactory$.class));
Field module = MultipartMixinFactory$.class.getDeclaredField("MODULE$");
assertEquals(Modifier.PUBLIC | Modifier.STATIC | Modifier.FINAL, module.getModifiers());
assertSame(MultipartMixinFactory$.class, module.getType());
assertEquals(1, MultipartMixinFactory$.class.getDeclaredFields().length);
assertTrue(Modifier.isPrivate(MultipartMixinFactory$.class.getDeclaredConstructor().getModifiers()));
ClassNode node = companion();
assertTrue(typeConstants(node, "<init>").contains("codechicken/multipart/TileMultipart"));
assertCall(node, "onCompiled", "codechicken/multipart/MultipartGenerator$", "registerTileClass");
}
@Test
void completesJavaTraitsWithAGuardedCopyFrom() throws Exception {
ClassNode node = companion();
Set<String> constants = constants(node, "autoCompleteJavaTrait");
assertTrue(constants.contains("copyFrom"));
assertTrue(constants.contains("(Lcodechicken/multipart/TileMultipart;)V"));
assertTrue(constants.contains("codechicken/multipart/TileMultipart"));
// The existing-method guard runs before anything is emitted.
assertCall(node, "autoCompleteJavaTrait", "codechicken/lib/asm/ASMHelper", "findMethod");
assertCall(node, "autoCompleteJavaTrait", "org/objectweb/asm/MethodVisitor", "visitMaxs");
}
@Test
void namesAndFillsThePassThroughTrait() throws Exception {
ClassNode node = companion();
Set<String> constants = constants(node, "generatePassThroughTrait");
for (String constant : new String[] { "T", "$$PassThrough", "impl", "L", ";", "bindPart",
"(Lcodechicken/multipart/TMultiPart;)V", "partRemoved", "(Lcodechicken/multipart/TMultiPart;I)V",
"canAddPart", "(Lcodechicken/multipart/TMultiPart;)Z", "<init>", "()V",
"codechicken/multipart/TileMultipart", "Unable to generate pass through trait for: ",
" class not found.", " is not an interface." }) {
assertTrue(constants.contains(constant), "missing constant " + constant);
}
assertCall(node, "generatePassThroughTrait", "codechicken/multipart/asm/ASMMixinCompiler$", "classNode");
assertCall(node, "generatePassThroughTrait", "codechicken/multipart/asm/ASMMixinCompiler$", "internalDefine");
assertCall(
node,
"generatePassThroughTrait",
"codechicken/multipart/asm/MultipartMixinFactory$",
"registerTrait");
assertCall(
node,
"codechicken$multipart$asm$MultipartMixinFactory$$generatePassThroughMethod$1",
"codechicken/multipart/asm/ASMMixinCompiler$",
"finishBridgeCall");
}
private static ClassNode companion() throws Exception {
ClassNode node = new ClassNode();
new ClassReader(MultipartMixinFactory$.class.getName()).accept(node, 0);
return node;
}
private static Set<String> constants(ClassNode node, String methodName) {
Set<String> found = new TreeSet<>();
for (AbstractInsnNode instruction : instructions(node, methodName)) {
if (instruction instanceof LdcInsnNode && ((LdcInsnNode) instruction).cst instanceof String) {
found.add((String) ((LdcInsnNode) instruction).cst);
}
}
return found;
}
private static Set<String> typeConstants(ClassNode node, String methodName) {
Set<String> found = new TreeSet<>();
for (AbstractInsnNode instruction : instructions(node, methodName)) {
if (instruction instanceof LdcInsnNode && ((LdcInsnNode) instruction).cst instanceof Type) {
found.add(((Type) ((LdcInsnNode) instruction).cst).getInternalName());
}
}
return found;
}
private static void assertCall(ClassNode node, String methodName, String owner, String name) {
for (AbstractInsnNode instruction : instructions(node, methodName)) {
if (instruction instanceof MethodInsnNode) {
MethodInsnNode call = (MethodInsnNode) instruction;
if (call.owner.equals(owner) && (name == null || call.name.equals(name))) return;
}
}
throw new AssertionError("Missing call " + owner + '.' + name + " in " + methodName);
}
private static AbstractInsnNode[] instructions(ClassNode node, String methodName) {
for (MethodNode method : node.methods) {
if (method.name.equals(methodName)) return method.instructions.toArray();
}
throw new AssertionError("Missing method " + methodName);
}
private static Set<String> publicMethods(Class<?> type) {
Set<String> result = new TreeSet<>();
for (Method method : type.getDeclaredMethods()) {
if (Modifier.isPublic(method.getModifiers())) {
result.add(method.getName() + Type.getMethodDescriptor(method));
}
}
return result;
}
private static Set<String> signatures(String... values) {
return new TreeSet<>(Arrays.asList(values));
}
}