diff --git a/build.gradle.kts b/build.gradle.kts index 31ca5ec38f..3846f07380 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,7 +2,11 @@ // // SPDX-License-Identifier: MPL-2.0 +import cc.tweaked.gradle.JUnitExt +import net.fabricmc.loom.api.LoomGradleExtensionAPI +import net.fabricmc.loom.util.gradle.SourceSetHelper import org.jetbrains.gradle.ext.compiler +import org.jetbrains.gradle.ext.runConfigurations import org.jetbrains.gradle.ext.settings plugins { @@ -38,6 +42,50 @@ githubRelease { tasks.publish { dependsOn(tasks.githubRelease) } +idea.project.settings.runConfigurations { + register("Core Tests") { + vmParameters = "-ea" + moduleName = "${idea.project.name}.core.test" + packageName = "" + } + + register("CraftOS Tests") { + vmParameters = "-ea" + moduleName = "${idea.project.name}.core.test" + className = "dan200.computercraft.core.ComputerTestDelegate" + } + + register("CraftOS Tests (Fast)") { + vmParameters = "-ea -Dcc.skip_keywords=slow" + moduleName = "${idea.project.name}.core.test" + className = "dan200.computercraft.core.ComputerTestDelegate" + } + + register("Common Tests") { + vmParameters = "-ea" + moduleName = "${idea.project.name}.common.test" + packageName = "" + } + + register("Fabric Tests") { + val fabricProject = evaluationDependsOn(":fabric") + val classPathGroup = fabricProject.extensions.getByType().mods + .joinToString(File.pathSeparator + File.pathSeparator) { modSettings -> + SourceSetHelper.getClasspath(modSettings, project).joinToString(File.pathSeparator) { it.absolutePath } + } + + vmParameters = "-ea -Dfabric.classPathGroups=$classPathGroup" + moduleName = "${idea.project.name}.fabric.test" + packageName = "" + } + + register("Forge Tests") { + vmParameters = "-ea" + moduleName = "${idea.project.name}.forge.test" + packageName = "" + } +} + idea.project.settings.compiler.javac { // We want ErrorProne to be present when compiling via IntelliJ, as it offers some helpful warnings // and errors. Loop through our source sets and find the appropriate flags. diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index cf01a5d8a4..eb53bbea2d 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -50,10 +50,11 @@ dependencies { implementation(libs.curseForgeGradle) implementation(libs.fabric.loom) implementation(libs.forgeGradle) + implementation(libs.ideaExt) implementation(libs.librarian) implementation(libs.minotaur) - implementation(libs.vineflower) implementation(libs.vanillaGradle) + implementation(libs.vineflower) } gradlePlugin { diff --git a/buildSrc/src/main/kotlin/cc/tweaked/gradle/CCTweakedPlugin.kt b/buildSrc/src/main/kotlin/cc/tweaked/gradle/CCTweakedPlugin.kt index 4700215145..5a5e27eb60 100644 --- a/buildSrc/src/main/kotlin/cc/tweaked/gradle/CCTweakedPlugin.kt +++ b/buildSrc/src/main/kotlin/cc/tweaked/gradle/CCTweakedPlugin.kt @@ -9,6 +9,10 @@ import org.gradle.api.Project import org.gradle.api.plugins.JavaPlugin import org.gradle.api.plugins.JavaPluginExtension import org.gradle.jvm.toolchain.JavaLanguageVersion +import org.gradle.plugins.ide.idea.model.IdeaModel +import org.jetbrains.gradle.ext.IdeaExtPlugin +import org.jetbrains.gradle.ext.runConfigurations +import org.jetbrains.gradle.ext.settings /** * Configures projects to match a shared configuration. @@ -21,6 +25,20 @@ class CCTweakedPlugin : Plugin { val sourceSets = project.extensions.getByType(JavaPluginExtension::class.java).sourceSets cct.sourceDirectories.add(SourceSetReference.internal(sourceSets.getByName("main"))) } + + project.plugins.withType(IdeaExtPlugin::class.java) { extendIdea(project) } + } + + /** + * Extend the [IdeaExtPlugin] plugin's `runConfiguration` container to also support [JUnitExt]. + */ + private fun extendIdea(project: Project) { + val ideaModel = project.extensions.findByName("idea") as IdeaModel? ?: return + val ideaProject = ideaModel.project ?: return + + ideaProject.settings.runConfigurations { + registerFactory(JUnitExt::class.java) { name -> project.objects.newInstance(JUnitExt::class.java, name) } + } } companion object { diff --git a/buildSrc/src/main/kotlin/cc/tweaked/gradle/IdeaExt.kt b/buildSrc/src/main/kotlin/cc/tweaked/gradle/IdeaExt.kt new file mode 100644 index 0000000000..320b6b252b --- /dev/null +++ b/buildSrc/src/main/kotlin/cc/tweaked/gradle/IdeaExt.kt @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package cc.tweaked.gradle + +import org.jetbrains.gradle.ext.JUnit +import javax.inject.Inject + +/** + * A version of [JUnit] with a functional [className]. + * + * See [#92](https://github.com/JetBrains/gradle-idea-ext-plugin/issues/92). + */ +open class JUnitExt @Inject constructor(nameParam: String) : JUnit(nameParam) { + override fun toMap(): MutableMap { + val map = HashMap(super.toMap()) + // Should be "class" instead of "className". + // See https://github.com/JetBrains/intellij-community/blob/9ba394021dc73a3926f13d6d6cdf434f9ee7046d/plugins/junit/src/com/intellij/execution/junit/JUnitRunConfigurationImporter.kt#L39 + map["class"] = className + return map + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index db44bf33b4..6ba787d35a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -45,7 +45,6 @@ rubidium = "0.6.1" sodium = "mc1.20-0.4.10" # Testing -byteBuddy = "1.14.7" hamcrest = "2.2" jqwik = "1.7.4" junit = "5.10.0" @@ -97,6 +96,7 @@ slf4j = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" } # Minecraft mods fabric-api = { module = "net.fabricmc.fabric-api:fabric-api", version.ref = "fabric-api" } fabric-loader = { module = "net.fabricmc:fabric-loader", version.ref = "fabric-loader" } +fabric-junit = { module = "net.fabricmc:fabric-loader-junit", version.ref = "fabric-loader" } fabricPermissions = { module = "me.lucko:fabric-permissions-api", version.ref = "fabricPermissions" } emi = { module = "dev.emi:emi-xplat-mojmap", version.ref = "emi" } iris = { module = "maven.modrinth:iris", version.ref = "iris" } @@ -114,8 +114,6 @@ rubidium = { module = "maven.modrinth:rubidium", version.ref = "rubidium" } sodium = { module = "maven.modrinth:sodium", version.ref = "sodium" } # Testing -byteBuddyAgent = { module = "net.bytebuddy:byte-buddy-agent", version.ref = "byteBuddy" } -byteBuddy = { module = "net.bytebuddy:byte-buddy", version.ref = "byteBuddy" } hamcrest = { module = "org.hamcrest:hamcrest", version.ref = "hamcrest" } jqwik-api = { module = "net.jqwik:jqwik-api", version.ref = "jqwik" } jqwik-engine = { module = "net.jqwik:jqwik-engine", version.ref = "jqwik" } @@ -135,6 +133,7 @@ errorProne-plugin = { module = "net.ltgt.gradle:gradle-errorprone-plugin", versi errorProne-testHelpers = { module = "com.google.errorprone:error_prone_test_helpers", version.ref = "errorProne-core" } fabric-loom = { module = "net.fabricmc:fabric-loom", version.ref = "fabric-loom" } forgeGradle = { module = "net.minecraftforge.gradle:ForgeGradle", version.ref = "forgeGradle" } +ideaExt = { module = "gradle.plugin.org.jetbrains.gradle.plugin.idea-ext:gradle-idea-ext", version.ref = "ideaExt" } kotlin-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } librarian = { module = "org.parchmentmc:librarian", version.ref = "librarian" } minotaur = { module = "com.modrinth.minotaur:Minotaur", version.ref = "minotaur" } @@ -154,7 +153,6 @@ vineflower = { module = "io.github.juuxel:loom-vineflower", version.ref = "vinef [plugins] forgeGradle = { id = "net.minecraftforge.gradle", version.ref = "forgeGradle" } githubRelease = { id = "com.github.breadmoirai.github-release", version.ref = "githubRelease" } -ideaExt = { id = "org.jetbrains.gradle.plugin.idea-ext", version.ref = "ideaExt" } kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } librarian = { id = "org.parchmentmc.librarian.forgegradle", version.ref = "librarian" } mixinGradle = { id = "org.spongepowered.mixin", version.ref = "mixinGradle" } diff --git a/projects/fabric/build.gradle.kts b/projects/fabric/build.gradle.kts index ba43ff08d8..773a29c162 100644 --- a/projects/fabric/build.gradle.kts +++ b/projects/fabric/build.gradle.kts @@ -87,10 +87,9 @@ dependencies { testModImplementation(testFixtures(project(":core"))) testModImplementation(testFixtures(project(":fabric"))) - testImplementation(libs.byteBuddy) - testImplementation(libs.byteBuddyAgent) testImplementation(libs.bundles.test) testRuntimeOnly(libs.bundles.testRuntime) + testRuntimeOnly(libs.fabric.junit) testFixturesImplementation(testFixtures(project(":core"))) } diff --git a/projects/fabric/src/test/java/dan200/computercraft/shared/FabricBootstrap.java b/projects/fabric/src/test/java/dan200/computercraft/shared/FabricBootstrap.java deleted file mode 100644 index 027ad83702..0000000000 --- a/projects/fabric/src/test/java/dan200/computercraft/shared/FabricBootstrap.java +++ /dev/null @@ -1,249 +0,0 @@ -// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers -// -// SPDX-License-Identifier: MPL-2.0 - -package dan200.computercraft.shared; - -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.bytebuddy.dynamic.loading.ClassInjector; -import net.fabricmc.api.EnvType; -import net.fabricmc.loader.impl.FabricLoaderImpl; -import net.fabricmc.loader.impl.game.minecraft.MinecraftGameProvider; -import net.fabricmc.loader.impl.game.minecraft.Slf4jLogHandler; -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 net.fabricmc.loader.impl.util.log.Log; -import org.junit.jupiter.api.extension.Extension; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.spongepowered.asm.mixin.MixinEnvironment; -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; -import java.util.stream.Collectors; - -/** - * Loads Fabric mods as part of this test run. - *

- * 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 { - private static final Logger LOG = LoggerFactory.getLogger(FabricBootstrap.class); - - public FabricBootstrap() throws ReflectiveOperationException, IOException { - Log.init(new Slf4jLogHandler()); - - 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); - } - - ByteBuddyAgent.install().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 final class BasicLauncher extends FabricLauncherBase { - private final List classpath = new ArrayList<>(); - - 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 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 getClassPath() { - return classpath; - } - } - - private static final class ClassTransformer implements ClassFileTransformer { - private final IMixinTransformer transformer; - private final Set definedClasses = new HashSet<>(); - private final Set generatedClasses; - - private ClassTransformer(IMixinTransformer transformer) { - this.transformer = transformer; - - try { - // As we can't hook into classloading itself, we need to track all the classes Mixin has generated. Yes, - // this is nasty. - var syntheticRegistryField = transformer.getClass().getDeclaredField("syntheticClassRegistry"); - syntheticRegistryField.setAccessible(true); - var syntheticRegistry = syntheticRegistryField.get(transformer); - - var classesField = syntheticRegistry.getClass().getDeclaredField("classes"); - classesField.setAccessible(true); - @SuppressWarnings("unchecked") var classes = (Map) classesField.get(syntheticRegistry); - generatedClasses = classes.keySet(); - } catch (ReflectiveOperationException e) { - throw new RuntimeException(e); - } - } - - @Override - public synchronized @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); - - // Keep track of all generated classes that we've seen, and define any new ones. We use ByteBuddy to inject - // the new class definitions, as doing it ourselves is hard. - if (generatedClasses.size() > definedClasses.size()) { - var toDefine = generatedClasses.stream().filter(definedClasses::add).collect(Collectors.toUnmodifiableMap( - genName -> genName.replace('/', '.'), - genName -> transformer.generateClass(MixinEnvironment.getDefaultEnvironment(), genName) - )); - - LOG.info("Defining {}", toDefine.keySet()); - try { - new ClassInjector.UsingReflection(loader).injectRaw(toDefine); - } catch (Exception e) { - LOG.error("Failed to define {}", className, e); - } - } - - return transformed == bytes ? null : transformed; - } - } -}