Skip to content

Commit

Permalink
Work on FML JUnit
Browse files Browse the repository at this point in the history
  • Loading branch information
shartte committed Dec 31, 2024
1 parent cde4bda commit 3868a20
Show file tree
Hide file tree
Showing 9 changed files with 135 additions and 150 deletions.
3 changes: 1 addition & 2 deletions junit/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ archivesBaseName = 'junit-fml'
dependencies {
implementation(platform("org.junit:junit-bom:$jupiter_version"))
implementation('org.junit.platform:junit-platform-launcher')
// BSL should not be exposed and the actual version should be provided by the neo dep
compileOnly("cpw.mods:bootstraplauncher:${project.bootstraplauncher_version}")
compileOnly(project(":loader"))
}

publishing {
Expand Down
14 changes: 0 additions & 14 deletions junit/src/main/java/module-info.java

This file was deleted.

53 changes: 48 additions & 5 deletions junit/src/main/java/net/neoforged/fml/junit/JUnitService.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,72 @@

package net.neoforged.fml.junit;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Set;
import net.neoforged.fml.loading.FMLLoader;
import net.neoforged.fml.startup.FmlInstrumentation;
import net.neoforged.fml.startup.StartupArgs;
import org.junit.platform.launcher.LauncherSession;
import org.junit.platform.launcher.LauncherSessionListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* A session listener for JUnit environments that will bootstrap a Minecraft (FML) environment.
*/
public class JUnitService implements LauncherSessionListener {
private ClassLoader oldLoader;
private static final Logger LOGGER = LoggerFactory.getLogger(JUnitService.class);

public JUnitService() {}
private Path gameDir;

@Override
public void launcherSessionOpened(LauncherSession session) {
// When the tests are started we want to make sure that they run on the transforming class loader which is set up by
// bootstrapping BSL which will then load the launch target
oldLoader = Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(LaunchWrapper.getTransformingLoader());
if (FMLLoader.currentOrNull() != null) {
throw new IllegalStateException("Another FML loader is already current.");
}

var instrumentation = FmlInstrumentation.obtainInstrumentation();

try {
gameDir = Files.createTempDirectory("fml_junit");
} catch (IOException e) {
throw new UncheckedIOException("Failed to create game directory", e);
}

FMLLoader.create(
instrumentation,
new StartupArgs(
gameDir,
gameDir.resolve(".cache"),
true,
null,
new String[] {},
Set.of(),
List.of(),
Thread.currentThread().getContextClassLoader()));
}

@Override
public void launcherSessionClosed(LauncherSession session) {
// Reset the loader in case JUnit wants to execute some pre-shutdown commands
// and our custom class loader might throw it off
Thread.currentThread().setContextClassLoader(oldLoader);
var current = FMLLoader.currentOrNull();
if (current != null) {
current.close();
}

if (gameDir != null) {
try {
Files.deleteIfExists(gameDir);
} catch (IOException e) {
throw new UncheckedIOException("Failed to delete temporary game directory", e);
}
}
}
}
38 changes: 0 additions & 38 deletions junit/src/main/java/net/neoforged/fml/junit/LaunchWrapper.java

This file was deleted.

This file was deleted.

3 changes: 2 additions & 1 deletion loader/src/main/java/net/neoforged/fml/startup/DevAgent.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ private DevAgent() {}
public static Instrumentation getInstrumentation() {
var stackWalker = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE);
var callingPackage = stackWalker.getCallerClass().getPackageName();
if (!callingPackage.equals(DevAgent.class.getPackage().getName())) {
if (!callingPackage.equals(DevAgent.class.getPackage().getName())
&& !callingPackage.equals("net.neoforged.fml.junit")) {
throw new IllegalStateException("This method may only be called by FML");
}
return instrumentation;
Expand Down
63 changes: 1 addition & 62 deletions loader/src/main/java/net/neoforged/fml/startup/Entrypoint.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.instrument.Instrumentation;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
Expand Down Expand Up @@ -55,7 +54,7 @@ protected static FMLLoader startup(String[] args, boolean headless, Dist forcedD
}
}

var instrumentation = obtainInstrumentation();
var instrumentation = FmlInstrumentation.obtainInstrumentation();

// Disabling JMX for JUnit improves startup time
if (System.getProperty("log4j2.disable.jmx") == null) {
Expand Down Expand Up @@ -113,66 +112,6 @@ private static String getArg(String[] args, String name, String defaultValue) {
return defaultValue;
}

private static Instrumentation obtainInstrumentation() {
var storedExceptions = new ArrayList<Exception>();

// Obtain instrumentation as early as possible. We use reflection here since we want to make sure that even if
// we are loaded through other means, we get the agent class from the system CL.
try {
return getFromOurOwnAgent();
} catch (Exception e) {
storedExceptions.add(e);
}

// Still don't have it? Try self-attach!
// This most likely will go away in the next Java LTS, but until then, it's convenient for unit tests.
try {
var classpathItem = SelfAttach.getClassPathItem();
var command = ProcessHandle.current().info().command().orElseThrow(() -> new RuntimeException("Could not self-attach: failed to determine our own commandline"));
var process = new ProcessBuilder(command, "-cp", classpathItem, SelfAttach.class.getName(), DevAgent.class.getName())
.redirectError(ProcessBuilder.Redirect.DISCARD)
.redirectOutput(ProcessBuilder.Redirect.DISCARD)
.inheritIO()
.start();
process.getOutputStream().close();
var result = process.waitFor();
if (result != 0) {
throw new RuntimeException("Could not self-attach agent: " + result);
}
return getFromOurOwnAgent();
} catch (Exception e) {
storedExceptions.add(e);
}

// If our own self-attach fails due to a user using an unexpected JVM, we support that they add ByteBuddy
// which has a plethora of self-attach options.
try {
var byteBuddyAgent = Class.forName("net.bytebuddy.agent.ByteBuddyAgent", true, ClassLoader.getSystemClassLoader());
var instrumentation = (Instrumentation) byteBuddyAgent.getMethod("install").invoke(null);
StartupLog.info("Using byte-buddy fallback");
return instrumentation;
} catch (Exception e) {
storedExceptions.add(e);
}

var e = new IllegalStateException("Failed to obtain instrumentation.");
storedExceptions.forEach(e::addSuppressed);
throw e;
}

private static Instrumentation getFromOurOwnAgent() throws Exception {
// This code may be surprising, but the DevAgent is *always* loaded on the system classloader.
// If we have been loaded somewhere beneath, our copy of DevAgent may not be the same. To ensure we actually
// get the "real" agent, we specifically grab the class from the system CL.
var devAgent = Class.forName("net.neoforged.fml.startup.DevAgent", true, ClassLoader.getSystemClassLoader());
var instrumentation = (Instrumentation) devAgent.getMethod("getInstrumentation").invoke(null);
StartupLog.info("Using our own agent");
if (instrumentation == null) {
throw new IllegalStateException("Our DevAgent was not attached. Pass an appropriate -javaagent parameter.");
}
return instrumentation;
}

/**
* Forces the log4j2 logging context to use the configuration shipped with fml_loader.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* Copyright (c) NeoForged and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/

package net.neoforged.fml.startup;

import java.lang.instrument.Instrumentation;
import java.util.ArrayList;

public final class FmlInstrumentation {
private FmlInstrumentation() {}

public static Instrumentation obtainInstrumentation() {
var stackWalker = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE);
var callingPackage = stackWalker.getCallerClass().getPackageName();
if (!callingPackage.equals(DevAgent.class.getPackage().getName())
&& !callingPackage.equals("net.neoforged.fml.junit")) {
throw new IllegalStateException("This method may only be called by FML");
}

var storedExceptions = new ArrayList<Exception>();

// Obtain instrumentation as early as possible. We use reflection here since we want to make sure that even if
// we are loaded through other means, we get the agent class from the system CL.
try {
return getFromOurOwnAgent();
} catch (Exception e) {
storedExceptions.add(e);
}

// Still don't have it? Try self-attach!
// This most likely will go away in the next Java LTS, but until then, it's convenient for unit tests.
try {
var classpathItem = SelfAttach.getClassPathItem();
var command = ProcessHandle.current().info().command().orElseThrow(() -> new RuntimeException("Could not self-attach: failed to determine our own commandline"));
var process = new ProcessBuilder(command, "-cp", classpathItem, SelfAttach.class.getName(), DevAgent.class.getName())
.redirectError(ProcessBuilder.Redirect.DISCARD)
.redirectOutput(ProcessBuilder.Redirect.DISCARD)
.inheritIO()
.start();
process.getOutputStream().close();
var result = process.waitFor();
if (result != 0) {
throw new RuntimeException("Could not self-attach agent: " + result);
}
return getFromOurOwnAgent();
} catch (Exception e) {
storedExceptions.add(e);
}

// If our own self-attach fails due to a user using an unexpected JVM, we support that they add ByteBuddy
// which has a plethora of self-attach options.
try {
var byteBuddyAgent = Class.forName("net.bytebuddy.agent.ByteBuddyAgent", true, ClassLoader.getSystemClassLoader());
var instrumentation = (Instrumentation) byteBuddyAgent.getMethod("install").invoke(null);
StartupLog.info("Using byte-buddy fallback");
return instrumentation;
} catch (Exception e) {
storedExceptions.add(e);
}

var e = new IllegalStateException("Failed to obtain instrumentation.");
storedExceptions.forEach(e::addSuppressed);
throw e;
}

private static Instrumentation getFromOurOwnAgent() throws Exception {
// This code may be surprising, but the DevAgent is *always* loaded on the system classloader.
// If we have been loaded somewhere beneath, our copy of DevAgent may not be the same. To ensure we actually
// get the "real" agent, we specifically grab the class from the system CL.
var devAgent = Class.forName("net.neoforged.fml.startup.DevAgent", true, ClassLoader.getSystemClassLoader());
var instrumentation = (Instrumentation) devAgent.getMethod("getInstrumentation").invoke(null);
StartupLog.info("Using our own agent");
if (instrumentation == null) {
throw new IllegalStateException("Our DevAgent was not attached. Pass an appropriate -javaagent parameter.");
}
return instrumentation;
}
}
3 changes: 3 additions & 0 deletions tests/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ neoForgeInstallations {
version = test_neoforge_version
minecraftVersion = test_minecraft_version
mainClass = "net.neoforged.fml.startup.Client"
// gameDir = "D:\\PrismInstances\\All the Mods 10 - ATM10\\minecraft"
gameDir = "D:\\PrismInstances\\Enigmatica 10 - E10\\minecraft"
}
register("server", fmlbuild.NeoForgeServerInstallation) {
version = test_neoforge_version
Expand Down Expand Up @@ -79,6 +81,7 @@ runConfigurations {
taskBefore tasks.named("installNeoForgeServer")
mainClass = "net.neoforged.fml.startup.Server"
jvmArguments.add(neoForgeInstallations.server.directory.dir("libraries").map { "-DlibraryDirectory=$it" })
workingDirectory = layout.projectDir.dir("Z:/TestServer")
}
}

Expand Down

0 comments on commit 3868a20

Please sign in to comment.