-
-
Save SquidDev/197d18817c2df2458545e0858f4c7b43 to your computer and use it in GitHub Desktop.
Inject Fabric into a JUnit instance
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import com.google.auto.service.AutoService; | |
import com.google.common.base.Splitter; | |
import com.google.common.io.ByteStreams; | |
import net.bytebuddy.agent.ByteBuddyAgent; | |
import net.fabricmc.api.EnvType; | |
import net.fabricmc.loader.impl.FabricLoaderImpl; | |
import net.fabricmc.loader.impl.game.minecraft.MinecraftGameProvider; | |
import net.fabricmc.loader.impl.launch.FabricLauncherBase; | |
import net.fabricmc.loader.impl.launch.FabricMixinBootstrap; | |
import net.fabricmc.loader.impl.launch.knot.MixinServiceKnot; | |
import net.fabricmc.loader.impl.transformer.FabricTransformer; | |
import net.fabricmc.loader.impl.util.LoaderUtil; | |
import org.junit.jupiter.api.extension.Extension; | |
import org.spongepowered.asm.mixin.transformer.IMixinTransformer; | |
import javax.annotation.Nullable; | |
import java.io.File; | |
import java.io.IOException; | |
import java.io.InputStream; | |
import java.lang.instrument.ClassFileTransformer; | |
import java.lang.instrument.IllegalClassFormatException; | |
import java.lang.instrument.Instrumentation; | |
import java.nio.file.Files; | |
import java.nio.file.Path; | |
import java.nio.file.Paths; | |
import java.security.ProtectionDomain; | |
import java.util.*; | |
import java.util.jar.Manifest; | |
/** | |
* Loads Fabric mods as part of this test run. | |
* <p> | |
* This sets up a minimalistic {@link FabricLauncherBase}, uses that to load mods, and then acquires an | |
* {@link Instrumentation} instance, registering a {@link ClassFileTransformer} to apply mixins and access wideners. | |
* | |
* @see net.fabricmc.loader.impl.launch.knot.Knot | |
*/ | |
@AutoService(Extension.class) | |
public class FabricBootstrap implements Extension { | |
public FabricBootstrap() throws ReflectiveOperationException, IOException { | |
var instrumentation = ByteBuddyAgent.install(); | |
readProperties(); | |
{ | |
var method = FabricLauncherBase.class.getDeclaredMethod("setProperties", Map.class); | |
method.setAccessible(true); | |
method.invoke(null, new HashMap<>()); | |
} | |
var provider = new MinecraftGameProvider(); | |
if (!provider.locateGame(new BasicLauncher(), new String[0])) { | |
throw new IllegalStateException("Cannot setup game"); | |
} | |
var loader = FabricLoaderImpl.INSTANCE; | |
loader.setGameProvider(provider); | |
loader.load(); | |
loader.freeze(); | |
loader.loadAccessWideners(); | |
FabricMixinBootstrap.init(EnvType.CLIENT, loader); | |
{ | |
var method = FabricLauncherBase.class.getDeclaredMethod("finishMixinBootstrapping"); | |
method.setAccessible(true); | |
method.invoke(null); | |
} | |
IMixinTransformer transformer; | |
{ | |
var method = MixinServiceKnot.class.getDeclaredMethod("getTransformer"); | |
method.setAccessible(true); | |
transformer = (IMixinTransformer) method.invoke(null); | |
} | |
instrumentation.addTransformer(new ClassTransformer(transformer)); | |
} | |
private static void readProperties() throws IOException { | |
try (var reader = Files.newBufferedReader(Path.of(".gradle/loom-cache/launch.cfg"))) { | |
var interesting = false; | |
String line; | |
while ((line = reader.readLine()) != null) { | |
if (line.startsWith(" ") || line.startsWith("\t")) { | |
if (!interesting) continue; | |
line = line.strip(); | |
var index = line.indexOf('='); | |
if (index >= 0) { | |
System.setProperty(line.substring(0, index), line.substring(index + 1)); | |
} else { | |
System.setProperty(line, ""); | |
} | |
} else { | |
interesting = line.equals("commonProperties") || line.equals("clientProperties"); | |
} | |
} | |
} | |
} | |
private static class BasicLauncher extends FabricLauncherBase { | |
private final List<Path> classpath = new ArrayList<>(); | |
private BasicLauncher() { | |
for (var entry : Splitter.on(File.pathSeparatorChar).split(System.getProperty("java.class.path"))) { | |
var path = Paths.get(entry); | |
if (Files.exists(path)) classpath.add(LoaderUtil.normalizeExistingPath(path)); | |
} | |
} | |
@Override | |
public void addToClassPath(Path path, String... allowedPrefixes) { | |
classpath.add(path); | |
} | |
@Override | |
public void setAllowedPrefixes(Path path, String... prefixes) { | |
} | |
@Override | |
public void setValidParentClassPath(Collection<Path> paths) { | |
throw new UnsupportedOperationException("setValidParentClassPath"); | |
} | |
@Override | |
public EnvType getEnvironmentType() { | |
return EnvType.CLIENT; | |
} | |
@Override | |
public boolean isClassLoaded(String name) { | |
return false; | |
} | |
@Override | |
public Class<?> loadIntoTarget(String name) { | |
throw new UnsupportedOperationException("loadIntoTarget"); | |
} | |
@Override | |
public ClassLoader getTargetClassLoader() { | |
return Thread.currentThread().getContextClassLoader(); | |
} | |
@Override | |
public @Nullable InputStream getResourceAsStream(String name) { | |
return BasicLauncher.class.getClassLoader().getResourceAsStream(name); | |
} | |
@Override | |
public @Nullable byte[] getClassByteArray(String name, boolean runTransformers) throws IOException { | |
try (var stream = BasicLauncher.class.getClassLoader().getResourceAsStream(LoaderUtil.getClassFileName(name))) { | |
if (stream == null) return null; | |
return ByteStreams.toByteArray(stream); | |
} | |
} | |
@Override | |
public Manifest getManifest(Path originPath) { | |
throw new UnsupportedOperationException("getManifest"); | |
} | |
@Override | |
public boolean isDevelopment() { | |
return true; | |
} | |
@Override | |
public String getEntrypoint() { | |
throw new UnsupportedOperationException("getEntrypoint"); | |
} | |
@Override | |
public String getTargetNamespace() { | |
return "named"; | |
} | |
@Override | |
public List<Path> getClassPath() { | |
return classpath; | |
} | |
} | |
private record ClassTransformer(IMixinTransformer transformer) implements ClassFileTransformer { | |
@Override | |
public @Nullable byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] bytes) throws IllegalClassFormatException { | |
var name = className.replace('/', '.'); | |
var transformed = FabricTransformer.transform(true, EnvType.CLIENT, name, bytes); | |
transformed = transformer.transformClassBytes(name, name, transformed); | |
return transformed == bytes ? null : transformed; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment