diff --git a/build.gradle b/build.gradle index 763627a95..f353266b7 100644 --- a/build.gradle +++ b/build.gradle @@ -75,12 +75,11 @@ allprojects { subprojects { subProject -> subProject.version = rootProject.version - jar.doFirst { + jar { manifest.attributes( 'Git-Commit' : gradleutils.gitInfo.abbreviatedId, 'Build-Number': "${subProject.version}", 'Automatic-Module-Name' : "fml_${subProject.name.replace("-", "_")}", - 'FMLModType' : 'LIBRARY', 'Specification-Title' : "FML${subProject.name}", 'Specification-Vendor' : 'NeoForged', 'Specification-Version' : "${subProject.version.toString().split('\\.')[0]}", @@ -88,6 +87,10 @@ subprojects { subProject -> 'Implementation-Version': "${subProject.version.toString().split('\\.')[0]}.${subProject.version.toString().split('\\.')[1]}", 'Implementation-Vendor' : 'NeoForged' ) + + if (subProject.name != 'junit') { + manifest.attributes.put('FMLModType', 'LIBRARY') + } } tasks.withType(JavaCompile) { diff --git a/junit/build.gradle b/junit/build.gradle new file mode 100644 index 000000000..72d611e16 --- /dev/null +++ b/junit/build.gradle @@ -0,0 +1,20 @@ +plugins { + id 'java-library' +} + +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}") +} + +publishing { + publications { + maven(MavenPublication) { + artifactId = archivesBaseName + } + } +} diff --git a/junit/src/main/java/module-info.java b/junit/src/main/java/module-info.java new file mode 100644 index 000000000..8fadf88d0 --- /dev/null +++ b/junit/src/main/java/module-info.java @@ -0,0 +1,14 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +import net.neoforged.fml.junit.JUnitService; +import org.junit.platform.launcher.LauncherSessionListener; + +module net.neoforged.fml.junit { + requires org.junit.platform.launcher; + requires cpw.mods.bootstraplauncher; + + provides LauncherSessionListener with JUnitService; +} diff --git a/junit/src/main/java/net/neoforged/fml/junit/JUnitService.java b/junit/src/main/java/net/neoforged/fml/junit/JUnitService.java new file mode 100644 index 000000000..ece1ccae5 --- /dev/null +++ b/junit/src/main/java/net/neoforged/fml/junit/JUnitService.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml.junit; + +import org.junit.platform.launcher.LauncherSession; +import org.junit.platform.launcher.LauncherSessionListener; + +/** + * A session listener for JUnit environments that will bootstrap a Minecraft (FML) environment. + */ +public class JUnitService implements LauncherSessionListener { + private ClassLoader oldLoader; + + public JUnitService() {} + + @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()); + } + + @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); + } +} diff --git a/junit/src/main/java/net/neoforged/fml/junit/LaunchWrapper.java b/junit/src/main/java/net/neoforged/fml/junit/LaunchWrapper.java new file mode 100644 index 000000000..2f412a24c --- /dev/null +++ b/junit/src/main/java/net/neoforged/fml/junit/LaunchWrapper.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml.junit; + +import cpw.mods.bootstraplauncher.BootstrapLauncher; +import java.nio.file.Files; +import java.nio.file.Path; + +public class LaunchWrapper { + private static ClassLoader transformingCL; + + /** + * Lazily get the transforming module class loader used by the game at runtime by loading the game when needed. + *

+ * Since in a JUnit environment we can't easily get the launch arguments, we will load them from the file specified via the {@code fml.junit.argsfile} system property. + */ + public static synchronized ClassLoader getTransformingLoader() { + if (transformingCL != null) return transformingCL; + final var oldLoader = Thread.currentThread().getContextClassLoader(); + + try { + final String[] args = Files.readAllLines(Path.of(System.getProperty("fml.junit.argsfile", "mainargs.txt"))).toArray(String[]::new); + BootstrapLauncher.unitTestingMain(args); + + transformingCL = Thread.currentThread().getContextClassLoader(); + } catch (Exception exception) { + System.err.println("Failed to start Minecraft: " + exception); + throw new RuntimeException(exception); + } finally { + Thread.currentThread().setContextClassLoader(oldLoader); + } + + return transformingCL; + } +} diff --git a/junit/src/main/resources/META-INF/services/org.junit.platform.launcher.LauncherSessionListener b/junit/src/main/resources/META-INF/services/org.junit.platform.launcher.LauncherSessionListener new file mode 100644 index 000000000..289fa811b --- /dev/null +++ b/junit/src/main/resources/META-INF/services/org.junit.platform.launcher.LauncherSessionListener @@ -0,0 +1 @@ +net.neoforged.fml.junit.JUnitService \ No newline at end of file diff --git a/loader/build.gradle b/loader/build.gradle index 281356ff8..e3362e5d3 100644 --- a/loader/build.gradle +++ b/loader/build.gradle @@ -51,6 +51,11 @@ dependencies { testImplementation("org.assertj:assertj-core:3.25.3") testImplementation("org.junit.jupiter:junit-jupiter-engine:$jupiter_version") testImplementation('com.google.jimfs:jimfs:1.3.0') + + // Provides the JUnit project as a BOM entry + constraints { + api(project(':junit')) + } } spotless { diff --git a/loader/src/main/java/net/neoforged/fml/loading/targets/JUnitDevLaunchTarget.java b/loader/src/main/java/net/neoforged/fml/loading/targets/JUnitDevLaunchTarget.java new file mode 100644 index 000000000..9ff067240 --- /dev/null +++ b/loader/src/main/java/net/neoforged/fml/loading/targets/JUnitDevLaunchTarget.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml.loading.targets; + +import net.neoforged.api.distmarker.Dist; + +/** + * A launch target for bootstrapping a slim Minecraft environment in forgedev, to be used in JUnit tests. + */ +public class JUnitDevLaunchTarget extends CommonDevLaunchHandler { + @Override + public Dist getDist() { + return Dist.DEDICATED_SERVER; + } + + @Override + protected void runService(String[] arguments, ModuleLayer gameLayer) throws Throwable { + Class.forName(gameLayer.findModule("neoforge").orElseThrow(), "net.neoforged.neoforge.junit.JUnitMain").getMethod("main", String[].class).invoke(null, (Object) arguments); + } + + @Override + public String name() { + return "forgejunitdev"; + } +} diff --git a/loader/src/main/java/net/neoforged/fml/loading/targets/JUnitUserDevLaunchTarget.java b/loader/src/main/java/net/neoforged/fml/loading/targets/JUnitUserDevLaunchTarget.java new file mode 100644 index 000000000..80e4362d7 --- /dev/null +++ b/loader/src/main/java/net/neoforged/fml/loading/targets/JUnitUserDevLaunchTarget.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml.loading.targets; + +import net.neoforged.api.distmarker.Dist; + +/** + * A launch target for bootstrapping a slim Minecraft environment in userdev, to be used in JUnit tests. + */ +public class JUnitUserDevLaunchTarget extends NeoForgeUserdevLaunchHandler { + @Override + public Dist getDist() { + return Dist.DEDICATED_SERVER; + } + + @Override + protected void runService(String[] arguments, ModuleLayer gameLayer) throws Throwable { + Class.forName(gameLayer.findModule("neoforge").orElseThrow(), "net.neoforged.neoforge.junit.JUnitMain").getMethod("main", String[].class).invoke(null, (Object) arguments); + } + + @Override + public String name() { + return "forgejunituserdev"; + } +} diff --git a/loader/src/main/resources/META-INF/services/cpw.mods.modlauncher.api.ILaunchHandlerService b/loader/src/main/resources/META-INF/services/cpw.mods.modlauncher.api.ILaunchHandlerService index 6b96d9a95..39231d1b7 100644 --- a/loader/src/main/resources/META-INF/services/cpw.mods.modlauncher.api.ILaunchHandlerService +++ b/loader/src/main/resources/META-INF/services/cpw.mods.modlauncher.api.ILaunchHandlerService @@ -6,3 +6,5 @@ net.neoforged.fml.loading.targets.NeoForgeServerDevLaunchHandler net.neoforged.fml.loading.targets.NeoForgeServerUserdevLaunchHandler net.neoforged.fml.loading.targets.NeoForgeDataDevLaunchHandler net.neoforged.fml.loading.targets.NeoForgeDataUserdevLaunchHandler +net.neoforged.fml.loading.targets.JUnitDevLaunchTarget +net.neoforged.fml.loading.targets.JUnitUserDevLaunchTarget diff --git a/settings.gradle b/settings.gradle index 574f77882..d63eca05f 100644 --- a/settings.gradle +++ b/settings.gradle @@ -24,3 +24,4 @@ rootProject.name = 'FancyModLoader' include 'loader' include 'earlydisplay' +include 'junit'