test: characterize TIconHitEffects

Freezes what is reachable without a client: getBreakingIcon falls back to
getBrokenIcon for the same side and ignores its subPart argument, destroy
effects request sides 0 through 5 in order, and the scaleDensity overload
decides whether the part's own bounds are consulted at all. The particle
calls need a world and an EffectRenderer, so actual appearance stays on
the manual checklist.

Adds a frozen Scala 2.11.5 consumer compiled against the reference dev jar
at 57130a6. It exercises both bridges: getBreakingIcon proves
JIconHitEffects$class delegates to the fixture's getBrokenIcon, and
addDestroyEffects proves TIconHitEffects$class resolves by collecting all
six icons before failing on the null tile.

49 plain-JVM tests, all passing against the untouched Scala.
This commit is contained in:
Algent committed 2026-08-14 21:12:52 +02:00
1 parent 57130a6955
commit 07ffc01db1
5 files changed
+260

No files matched your search

@@ -0,0 +1,100 @@
package codechicken.multipart;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import net.minecraft.util.IIcon;
import org.junit.jupiter.api.Test;
import codechicken.lib.vec.Cuboid6;
/**
* The particle calls themselves need a world and an EffectRenderer, so they cannot run headless. What is asserted here
* is the part of the contract that is reachable: icon selection, the order and range of the icons requested, and
* whether the part's own bounds are consulted. Actual particle appearance stays on the manual checklist.
*/
class IconHitEffectsCharacterizationTest {
@Test
void breakingIconFallsBackToTheBrokenIconForThatSide() {
SpyPart part = new SpyPart();
assertNull(part.getBreakingIcon("ignored-subpart", 3));
assertEquals(1, part.brokenIconSides.size());
assertEquals(3, part.brokenIconSides.get(0));
}
@Test
void deprecatedBridgeMatchesTheBreakingIconFallback() {
SpyPart part = new SpyPart();
assertNull(JIconHitEffects$class.getBreakingIcon(part, null, 5));
assertEquals(1, part.brokenIconSides.size());
assertEquals(5, part.brokenIconSides.get(0));
}
@Test
void destroyEffectsRequestAllSixSidesInOrderAndScaleToTheBounds() {
SpyPart part = new SpyPart();
// The tile is null, so this reaches part.tile after collecting the icons and bounds.
assertThrows(NullPointerException.class, () -> IconHitEffects.addDestroyEffects(part, null));
assertEquals(Arrays.asList(0, 1, 2, 3, 4, 5), part.brokenIconSides);
assertEquals(1, part.boundsRequested);
}
@Test
void destroyEffectsWithoutDensityScalingIgnoreTheBounds() {
SpyPart part = new SpyPart();
assertThrows(NullPointerException.class, () -> IconHitEffects.addDestroyEffects(part, null, false));
assertEquals(Arrays.asList(0, 1, 2, 3, 4, 5), part.brokenIconSides);
assertEquals(0, part.boundsRequested);
}
@Test
void iconHitEffectsPartIsAMultiPart() {
SpyPart part = new SpyPart();
assertTrue(part instanceof JIconHitEffects);
assertTrue(part instanceof TMultiPart);
}
/** Mirrors the forwarder Scala emits for a class that does not override getBreakingIcon. */
private static final class SpyPart extends TMultiPart implements JIconHitEffects {
private final List<Integer> brokenIconSides = new ArrayList<>();
private int boundsRequested;
@Override
public String getType() {
return "test:icon-hit";
}
@Override
public Cuboid6 getBounds() {
boundsRequested++;
return new Cuboid6(0, 0, 0, 1, 1, 1);
}
@Override
public IIcon getBrokenIcon(int side) {
brokenIconSides.add(side);
return null;
}
@Override
public IIcon getBreakingIcon(Object subPart, int side) {
return JIconHitEffects$class.getBreakingIcon(this, subPart, side);
}
}
}
@@ -0,0 +1,68 @@
package codechicken.multipart;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Objects;
import java.util.Scanner;
import net.minecraft.client.particle.EffectRenderer;
import org.junit.jupiter.api.Test;
/**
* Loads a Scala class compiled against the reference dev jar. Its forwarders call all five statics across both
* JIconHitEffects$class and TIconHitEffects$class.
*/
class TIconHitEffectsBinaryCompatibilityTest {
@Test
void referenceScalaImplementorStillLinksThroughBothBridges() throws Exception {
Class<?> fixtureClass = new FixtureClassLoader(TIconHitEffects.class.getClassLoader()).define(loadFixture());
Object fixture = fixtureClass.getDeclaredConstructor().newInstance();
assertTrue(fixture instanceof TIconHitEffects);
assertTrue(fixture instanceof JIconHitEffects);
assertTrue(fixture instanceof TMultiPart);
TIconHitEffects part = (TIconHitEffects) fixture;
// Runs JIconHitEffects$class.getBreakingIcon, which must delegate to the fixture's getBrokenIcon.
assertNull(part.getBreakingIcon(null, 4));
assertEquals(4, fixtureClass.getMethod("lastSide").invoke(fixture));
// Runs TIconHitEffects$class.addDestroyEffects. The fixture's tile is null, so it collects all six icons and
// then fails on the tile; reaching that point proves the static resolved rather than failing to link.
InvocationTargetException thrown = assertThrows(
InvocationTargetException.class,
() -> fixtureClass.getMethod("addDestroyEffects", EffectRenderer.class).invoke(fixture, (Object) null));
assertInstanceOf(NullPointerException.class, thrown.getCause());
assertEquals(5, fixtureClass.getMethod("lastSide").invoke(fixture));
}
private static byte[] loadFixture() {
InputStream input = Objects.requireNonNull(
TIconHitEffectsBinaryCompatibilityTest.class
.getResourceAsStream("/compat/ReferenceScalaIconHitEffects.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);
}
}
}