From 749ac0edd0053c499653e12f66723f96bdbc6b1e Mon Sep 17 00:00:00 2001 From: James Fredley Date: Tue, 17 Feb 2026 10:18:57 -0500 Subject: [PATCH 1/3] Propagate Java toolchain to JavaExec tasks Gradle's JavaPlugin sets toolchain conventions on JavaCompile, Javadoc, and Test tasks but not on JavaExec tasks. This means forked JVM processes (dbm-* migration tasks, console, shell, application context commands) use the JDK running Gradle instead of the project's configured toolchain. Add configureToolchainForForkTasks() to GrailsGradlePlugin that propagates the project toolchain to all JavaExec tasks via javaLauncher.convention(). Uses convention() so individual tasks can still override via set(). Only activates when java.toolchain.languageVersion is explicitly configured, preserving existing behavior for users without toolchains. Test coverage: 8 JUnit Jupiter tests via Gradle TestKit covering toolchain propagation (JavaExec, Test, ApplicationContextCommandTask, convention override), backwards compatibility (no-toolchain), and fork settings preservation (system properties, heap sizes). --- grails-gradle/plugins/build.gradle | 10 + .../plugin/core/GrailsGradlePlugin.groovy | 39 ++ .../core/GrailsGradlePluginToolchainTest.java | 399 ++++++++++++++++++ 3 files changed, 448 insertions(+) create mode 100644 grails-gradle/plugins/src/test/java/org/grails/gradle/plugin/core/GrailsGradlePluginToolchainTest.java diff --git a/grails-gradle/plugins/build.gradle b/grails-gradle/plugins/build.gradle index 3772fe07f38..e41a9cb92cf 100644 --- a/grails-gradle/plugins/build.gradle +++ b/grails-gradle/plugins/build.gradle @@ -56,6 +56,15 @@ dependencies { implementation 'org.springframework.boot:spring-boot-gradle-plugin' implementation 'org.springframework.boot:spring-boot-loader-tools' implementation 'io.spring.gradle:dependency-management-plugin' + + // Testing - Gradle TestKit is auto-added by java-gradle-plugin + // NOTE: Cannot use Spock here because grails-gradle-tasks transitively + // pulls org.apache.groovy:groovy:4.0.30 which conflicts with + // org.codehaus.groovy:groovy:3.0.25 (Gradle's built-in Groovy). + // Tests use JUnit Jupiter in Java instead. + testImplementation 'org.junit.jupiter:junit-jupiter-api' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } gradlePlugin { @@ -131,4 +140,5 @@ tasks.withType(Copy) { apply { from rootProject.layout.projectDirectory.file('gradle/docs-config.gradle') + from rootProject.layout.projectDirectory.file('gradle/test-config.gradle') } diff --git a/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsGradlePlugin.groovy b/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsGradlePlugin.groovy index 123a2b76d31..3cddfc86f32 100644 --- a/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsGradlePlugin.groovy +++ b/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsGradlePlugin.groovy @@ -54,6 +54,7 @@ import org.gradle.api.tasks.TaskContainer import org.gradle.api.tasks.TaskProvider import org.gradle.api.tasks.compile.GroovyCompile import org.gradle.api.tasks.testing.Test +import org.gradle.jvm.toolchain.JavaToolchainService import org.gradle.language.jvm.tasks.ProcessResources import org.gradle.process.JavaForkOptions import org.gradle.tooling.provider.model.ToolingModelBuilderRegistry @@ -556,6 +557,44 @@ class GrailsGradlePlugin extends GroovyPlugin { String grailsEnvSystemProperty = System.getProperty(Environment.KEY) tasks.withType(Test).configureEach(systemPropertyConfigurer.curry(grailsEnvSystemProperty ?: Environment.TEST.getName())) tasks.withType(JavaExec).configureEach(systemPropertyConfigurer.curry(grailsEnvSystemProperty ?: Environment.DEVELOPMENT.getName())) + + configureToolchainForForkTasks(project) + } + + /** + * Configures {@link JavaExec} tasks to inherit the project's Java toolchain. + * + *

Gradle's {@code JavaPlugin} already sets toolchain conventions on + * {@code JavaCompile}, {@code Javadoc}, and {@code Test} tasks, but does + * not set them on {@code JavaExec} tasks. This means forked + * JVM processes (dbm-* migration tasks, console, shell, and application + * context commands) use the JDK running Gradle instead of the project's + * configured toolchain. When the project targets a different JDK version + * than the one running Gradle, this causes {@code UnsupportedClassVersionError} + * or silent runtime failures.

+ * + *

This method only acts when the user has explicitly configured a toolchain + * via {@code java.toolchain.languageVersion}. When no toolchain is configured, + * behavior is unchanged - tasks use the JDK running Gradle as before.

+ * + *

Uses {@code convention()} so that individual tasks can still override + * the launcher via {@code javaLauncher.set(...)} if needed.

+ * + * @param project the Gradle project + * @since 7.0.8 + */ + protected void configureToolchainForForkTasks(Project project) { + project.afterEvaluate { + def javaExtension = project.extensions.findByType(org.gradle.api.plugins.JavaPluginExtension) + if (javaExtension?.toolchain?.languageVersion?.isPresent()) { + def toolchainService = project.extensions.getByType(JavaToolchainService) + def launcher = toolchainService.launcherFor(javaExtension.toolchain) + + project.tasks.withType(JavaExec).configureEach { JavaExec task -> + task.javaLauncher.convention(launcher) + } + } + } } protected void configureConsoleTask(Project project) { diff --git a/grails-gradle/plugins/src/test/java/org/grails/gradle/plugin/core/GrailsGradlePluginToolchainTest.java b/grails-gradle/plugins/src/test/java/org/grails/gradle/plugin/core/GrailsGradlePluginToolchainTest.java new file mode 100644 index 00000000000..fe23c48abbb --- /dev/null +++ b/grails-gradle/plugins/src/test/java/org/grails/gradle/plugin/core/GrailsGradlePluginToolchainTest.java @@ -0,0 +1,399 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.gradle.plugin.core; + +import org.gradle.testkit.runner.BuildResult; +import org.gradle.testkit.runner.GradleRunner; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Properties; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Tests that {@link GrailsGradlePlugin} propagates the project's Java toolchain + * to JavaExec tasks via {@code javaLauncher.convention()}. + * + *

Without the fix, JavaExec tasks spawned by Grails (dbm-* migration + * commands, console, shell, application context commands) use the JDK + * running Gradle instead of the project's configured toolchain. This + * causes {@code UnsupportedClassVersionError} when the project targets + * a different JDK version than the one running Gradle.

+ * + *

NOTE: Cannot use Spock here because grails-gradle-tasks transitively + * pulls {@code org.apache.groovy:groovy:4.0.30} which conflicts with + * Gradle's built-in {@code org.codehaus.groovy:groovy:3.0.25}.

+ * + * @since 7.0.8 + */ +class GrailsGradlePluginToolchainTest { + + @TempDir + Path projectDir; + + private static final int CURRENT_JDK = Runtime.version().feature(); + private static final String GRAILS_VERSION = resolveGrailsVersion(); + + /** + * Reads {@code projectVersion} from the root {@code gradle.properties} so the + * test stays in sync with the actual build and is not tied to a hardcoded version. + */ + private static String resolveGrailsVersion() { + Properties props = new Properties(); + try (InputStream in = GrailsGradlePluginToolchainTest.class + .getClassLoader().getResourceAsStream("grails-gradle-plugins-project.properties")) { + if (in != null) { + props.load(in); + String v = props.getProperty("projectVersion"); + if (v != null && !v.isEmpty()) return v; + } + } catch (IOException ignored) { } + // Fallback: read from root gradle.properties relative to working directory + try { + Path root = Path.of(System.getProperty("user.dir")); + // Walk up until we find gradle.properties with projectVersion + for (Path dir = root; dir != null; dir = dir.getParent()) { + Path gp = dir.resolve("gradle.properties"); + if (Files.exists(gp)) { + Properties rootProps = new Properties(); + try (var reader = Files.newBufferedReader(gp)) { + rootProps.load(reader); + } + String v = rootProps.getProperty("projectVersion"); + if (v != null && !v.isEmpty()) return v; + } + } + } catch (IOException ignored) { } + return "7.0.8-SNAPSHOT"; + } + + @BeforeEach + void setup() throws IOException { + // Minimal Grails directory structure required by the plugin + Files.createDirectories(projectDir.resolve("grails-app/conf")); + Files.writeString(projectDir.resolve("grails-app/conf/application.yml"), ""); + Files.writeString(projectDir.resolve("settings.gradle"), "rootProject.name = 'test-toolchain'\n"); + // GrailsGradlePlugin.resolveGrailsVersion() requires this property + Files.writeString(projectDir.resolve("gradle.properties"), "grailsVersion=" + GRAILS_VERSION + "\n"); + } + + private BuildResult runBuild(String... args) { + return GradleRunner.create() + .withProjectDir(projectDir.toFile()) + .withArguments(args) + .withPluginClasspath() + .forwardOutput() + .build(); + } + + // ---------------------------------------------------------------- + // Toolchain propagation tests + // ---------------------------------------------------------------- + + @Nested + @DisplayName("With toolchain configured") + class WithToolchain { + + @Test + @DisplayName("JavaExec tasks inherit project toolchain") + void javaExecInheritsToolchain() throws IOException { + Files.writeString(projectDir.resolve("build.gradle"), + "plugins {\n" + + " id 'org.apache.grails.gradle.grails-app'\n" + + "}\n" + + "\n" + + "java {\n" + + " toolchain {\n" + + " languageVersion = JavaLanguageVersion.of(" + CURRENT_JDK + ")\n" + + " }\n" + + "}\n" + + "\n" + + "tasks.register('printLauncher', JavaExec) {\n" + + " classpath = files()\n" + + " mainClass = 'does.not.Matter'\n" + + "}\n" + + "\n" + + "tasks.register('checkToolchain') {\n" + + " doLast {\n" + + " def launcher = tasks.named('printLauncher', JavaExec).get().javaLauncher\n" + + " if (launcher.isPresent()) {\n" + + " def metadata = launcher.get().metadata\n" + + " println \"TOOLCHAIN_VERSION=${metadata.languageVersion.asInt()}\"\n" + + " } else {\n" + + " println 'TOOLCHAIN_VERSION=none'\n" + + " }\n" + + " }\n" + + "}\n" + ); + + BuildResult result = runBuild("checkToolchain", "--stacktrace"); + assertTrue(result.getOutput().contains("TOOLCHAIN_VERSION=" + CURRENT_JDK), + "JavaExec task should inherit project toolchain (JDK " + CURRENT_JDK + ")"); + } + + @Test + @DisplayName("Test tasks inherit project toolchain") + void testTaskInheritsToolchain() throws IOException { + Files.writeString(projectDir.resolve("build.gradle"), + "plugins {\n" + + " id 'org.apache.grails.gradle.grails-app'\n" + + "}\n" + + "\n" + + "java {\n" + + " toolchain {\n" + + " languageVersion = JavaLanguageVersion.of(" + CURRENT_JDK + ")\n" + + " }\n" + + "}\n" + + "\n" + + "tasks.register('checkTestToolchain') {\n" + + " doLast {\n" + + " def testTask = tasks.named('test', Test).get()\n" + + " def launcher = testTask.javaLauncher\n" + + " if (launcher.isPresent()) {\n" + + " println \"TEST_TOOLCHAIN_VERSION=${launcher.get().metadata.languageVersion.asInt()}\"\n" + + " } else {\n" + + " println 'TEST_TOOLCHAIN_VERSION=none'\n" + + " }\n" + + " }\n" + + "}\n" + ); + + BuildResult result = runBuild("checkTestToolchain", "--stacktrace"); + assertTrue(result.getOutput().contains("TEST_TOOLCHAIN_VERSION=" + CURRENT_JDK), + "Test task should inherit project toolchain (JDK " + CURRENT_JDK + ")"); + } + + @Test + @DisplayName("ApplicationContextCommandTask inherits toolchain via grails-web plugin") + void applicationContextCommandTaskInheritsToolchain() throws IOException { + Files.writeString(projectDir.resolve("build.gradle"), + "plugins {\n" + + " id 'org.apache.grails.gradle.grails-web'\n" + + "}\n" + + "\n" + + "java {\n" + + " toolchain {\n" + + " languageVersion = JavaLanguageVersion.of(" + CURRENT_JDK + ")\n" + + " }\n" + + "}\n" + + "\n" + + "tasks.register('checkCommandToolchain') {\n" + + " doLast {\n" + + " def cmdTask = tasks.named('urlMappingsReport').get()\n" + + " if (cmdTask instanceof JavaExec) {\n" + + " def launcher = ((JavaExec) cmdTask).javaLauncher\n" + + " if (launcher.isPresent()) {\n" + + " println \"CMD_TOOLCHAIN_VERSION=${launcher.get().metadata.languageVersion.asInt()}\"\n" + + " } else {\n" + + " println 'CMD_TOOLCHAIN_VERSION=none'\n" + + " }\n" + + " } else {\n" + + " println 'CMD_TOOLCHAIN_VERSION=not_javaexec'\n" + + " }\n" + + " }\n" + + "}\n" + ); + + BuildResult result = runBuild("checkCommandToolchain", "--stacktrace"); + assertTrue(result.getOutput().contains("CMD_TOOLCHAIN_VERSION=" + CURRENT_JDK), + "ApplicationContextCommandTask should inherit project toolchain (JDK " + CURRENT_JDK + ")"); + } + + @Test + @DisplayName("convention() allows individual task override via set()") + void conventionAllowsOverride() throws IOException { + Files.writeString(projectDir.resolve("build.gradle"), + "import org.gradle.jvm.toolchain.JavaLanguageVersion\n" + + "\n" + + "plugins {\n" + + " id 'org.apache.grails.gradle.grails-app'\n" + + "}\n" + + "\n" + + "java {\n" + + " toolchain {\n" + + " languageVersion = JavaLanguageVersion.of(" + CURRENT_JDK + ")\n" + + " }\n" + + "}\n" + + "\n" + + "tasks.register('customExec', JavaExec) {\n" + + " classpath = files()\n" + + " mainClass = 'does.not.Matter'\n" + + " javaLauncher.set(javaToolchains.launcherFor {\n" + + " languageVersion = JavaLanguageVersion.of(" + CURRENT_JDK + ")\n" + + " })\n" + + "}\n" + + "\n" + + "tasks.register('checkOverride') {\n" + + " doLast {\n" + + " def launcher = tasks.named('customExec', JavaExec).get().javaLauncher\n" + + " if (launcher.isPresent()) {\n" + + " println \"OVERRIDE_VERSION=${launcher.get().metadata.languageVersion.asInt()}\"\n" + + " } else {\n" + + " println 'OVERRIDE_VERSION=none'\n" + + " }\n" + + " }\n" + + "}\n" + ); + + BuildResult result = runBuild("checkOverride", "--stacktrace"); + assertTrue(result.getOutput().contains("OVERRIDE_VERSION=" + CURRENT_JDK), + "Task with explicit set() should override convention"); + } + } + + // ---------------------------------------------------------------- + // No-toolchain backwards compatibility tests + // ---------------------------------------------------------------- + + @Nested + @DisplayName("Without toolchain configured (backwards compatibility)") + class WithoutToolchain { + + @Test + @DisplayName("JavaExec tasks work without errors when no toolchain configured") + void javaExecWorksWithoutToolchain() throws IOException { + Files.writeString(projectDir.resolve("build.gradle"), + "plugins {\n" + + " id 'org.apache.grails.gradle.grails-app'\n" + + "}\n" + + "\n" + + "tasks.register('printLauncher', JavaExec) {\n" + + " classpath = files()\n" + + " mainClass = 'does.not.Matter'\n" + + "}\n" + + "\n" + + "tasks.register('checkToolchain') {\n" + + " doLast {\n" + + " def launcher = tasks.named('printLauncher', JavaExec).get().javaLauncher\n" + + " if (launcher.isPresent()) {\n" + + " println 'HAS_LAUNCHER=true'\n" + + " } else {\n" + + " println 'HAS_LAUNCHER=false'\n" + + " }\n" + + " }\n" + + "}\n" + ); + + BuildResult result = runBuild("checkToolchain", "--stacktrace"); + assertTrue(result.getOutput().contains("HAS_LAUNCHER="), + "Plugin should not error when no toolchain is configured"); + } + + @Test + @DisplayName("GrailsWebGradlePlugin works without errors when no toolchain configured") + void webPluginWorksWithoutToolchain() throws IOException { + Files.writeString(projectDir.resolve("build.gradle"), + "plugins {\n" + + " id 'org.apache.grails.gradle.grails-web'\n" + + "}\n" + + "\n" + + "tasks.register('checkNoError') {\n" + + " doLast {\n" + + " println 'WEB_PLUGIN_OK=true'\n" + + " }\n" + + "}\n" + ); + + BuildResult result = runBuild("checkNoError", "--stacktrace"); + assertTrue(result.getOutput().contains("WEB_PLUGIN_OK=true"), + "GrailsWebGradlePlugin should work without toolchain configured"); + } + } + + // ---------------------------------------------------------------- + // Fork settings tests (system properties, heap sizes) + // ---------------------------------------------------------------- + + @Nested + @DisplayName("Fork settings (existing behavior preserved)") + class ForkSettings { + + @Test + @DisplayName("configureForkSettings applies system properties and default heap sizes") + void forkSettingsApplyDefaults() throws IOException { + Files.writeString(projectDir.resolve("build.gradle"), + "plugins {\n" + + " id 'org.apache.grails.gradle.grails-app'\n" + + "}\n" + + "\n" + + "tasks.register('checkSysProps', JavaExec) {\n" + + " classpath = files()\n" + + " mainClass = 'does.not.Matter'\n" + + "}\n" + + "\n" + + "tasks.register('inspectSysProps') {\n" + + " doLast {\n" + + " def task = tasks.named('checkSysProps', JavaExec).get()\n" + + " def sysProps = task.systemProperties\n" + + " println \"HAS_ENV=${sysProps.containsKey('grails.env')}\"\n" + + " println \"MIN_HEAP=${task.minHeapSize}\"\n" + + " println \"MAX_HEAP=${task.maxHeapSize}\"\n" + + " }\n" + + "}\n" + ); + + BuildResult result = runBuild("inspectSysProps", "--stacktrace"); + assertTrue(result.getOutput().contains("HAS_ENV=true"), + "Fork settings should set grails.env system property"); + assertTrue(result.getOutput().contains("MIN_HEAP=768m"), + "Default min heap should be 768m"); + assertTrue(result.getOutput().contains("MAX_HEAP=768m"), + "Default max heap should be 768m"); + } + + @Test + @DisplayName("Custom heap sizes are not overridden by fork settings") + void customHeapSizesPreserved() throws IOException { + Files.writeString(projectDir.resolve("build.gradle"), + "plugins {\n" + + " id 'org.apache.grails.gradle.grails-app'\n" + + "}\n" + + "\n" + + "tasks.register('customHeap', JavaExec) {\n" + + " classpath = files()\n" + + " mainClass = 'does.not.Matter'\n" + + " minHeapSize = '512m'\n" + + " maxHeapSize = '2g'\n" + + "}\n" + + "\n" + + "tasks.register('inspectHeap') {\n" + + " doLast {\n" + + " def task = tasks.named('customHeap', JavaExec).get()\n" + + " println \"MIN_HEAP=${task.minHeapSize}\"\n" + + " println \"MAX_HEAP=${task.maxHeapSize}\"\n" + + " }\n" + + "}\n" + ); + + BuildResult result = runBuild("inspectHeap", "--stacktrace"); + assertTrue(result.getOutput().contains("MIN_HEAP=512m"), + "Custom min heap should be preserved"); + assertTrue(result.getOutput().contains("MAX_HEAP=2g"), + "Custom max heap should be preserved"); + } + } +} From fec38f4908359693d065a449e184dfe55ea79fa4 Mon Sep 17 00:00:00 2001 From: James Fredley Date: Wed, 18 Feb 2026 19:14:03 -0500 Subject: [PATCH 2/3] Rewrite toolchain tests to Spock with file-based fixtures Address reviewer feedback: migrate from JUnit Jupiter to Spock, replace inline build scripts with file-based test project fixtures, add GradleSpecification base class for Gradle TestKit tests, pass projectVersion via Gradle test config, and exclude Groovy 4 from test classpath to resolve the Spock/Gradle Groovy conflict. Assisted-by: Claude Code --- grails-gradle/plugins/build.gradle | 18 +- .../plugin/core/GradleSpecification.groovy | 134 ++++++ .../GrailsGradlePluginToolchainSpec.groovy | 136 ++++++ .../core/GrailsGradlePluginToolchainTest.java | 399 ------------------ .../fork-settings-custom/build.gradle | 18 + .../fork-settings-custom/gradle.properties | 1 + .../grails-app/conf/application.yml | 0 .../fork-settings-custom/settings.gradle | 1 + .../fork-settings-defaults/build.gradle | 18 + .../fork-settings-defaults/gradle.properties | 1 + .../grails-app/conf/application.yml | 0 .../fork-settings-defaults/settings.gradle | 1 + .../no-toolchain-javaexec/build.gradle | 19 + .../no-toolchain-javaexec/gradle.properties | 1 + .../grails-app/conf/application.yml | 0 .../no-toolchain-javaexec/settings.gradle | 1 + .../no-toolchain-web/build.gradle | 9 + .../no-toolchain-web/gradle.properties | 1 + .../grails-app/conf/application.yml | 0 .../no-toolchain-web/settings.gradle | 1 + .../toolchain-command/build.gradle | 25 ++ .../toolchain-command/gradle.properties | 1 + .../grails-app/conf/application.yml | 0 .../toolchain-command/settings.gradle | 1 + .../toolchain-javaexec/build.gradle | 26 ++ .../toolchain-javaexec/gradle.properties | 1 + .../grails-app/conf/application.yml | 0 .../toolchain-javaexec/settings.gradle | 1 + .../toolchain-override/build.gradle | 30 ++ .../toolchain-override/gradle.properties | 1 + .../grails-app/conf/application.yml | 0 .../toolchain-override/settings.gradle | 1 + .../test-projects/toolchain-test/build.gradle | 21 + .../toolchain-test/gradle.properties | 1 + .../grails-app/conf/application.yml | 0 .../toolchain-test/settings.gradle | 1 + 36 files changed, 464 insertions(+), 405 deletions(-) create mode 100644 grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/core/GradleSpecification.groovy create mode 100644 grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/core/GrailsGradlePluginToolchainSpec.groovy delete mode 100644 grails-gradle/plugins/src/test/java/org/grails/gradle/plugin/core/GrailsGradlePluginToolchainTest.java create mode 100644 grails-gradle/plugins/src/test/resources/test-projects/fork-settings-custom/build.gradle create mode 100644 grails-gradle/plugins/src/test/resources/test-projects/fork-settings-custom/gradle.properties create mode 100644 grails-gradle/plugins/src/test/resources/test-projects/fork-settings-custom/grails-app/conf/application.yml create mode 100644 grails-gradle/plugins/src/test/resources/test-projects/fork-settings-custom/settings.gradle create mode 100644 grails-gradle/plugins/src/test/resources/test-projects/fork-settings-defaults/build.gradle create mode 100644 grails-gradle/plugins/src/test/resources/test-projects/fork-settings-defaults/gradle.properties create mode 100644 grails-gradle/plugins/src/test/resources/test-projects/fork-settings-defaults/grails-app/conf/application.yml create mode 100644 grails-gradle/plugins/src/test/resources/test-projects/fork-settings-defaults/settings.gradle create mode 100644 grails-gradle/plugins/src/test/resources/test-projects/no-toolchain-javaexec/build.gradle create mode 100644 grails-gradle/plugins/src/test/resources/test-projects/no-toolchain-javaexec/gradle.properties create mode 100644 grails-gradle/plugins/src/test/resources/test-projects/no-toolchain-javaexec/grails-app/conf/application.yml create mode 100644 grails-gradle/plugins/src/test/resources/test-projects/no-toolchain-javaexec/settings.gradle create mode 100644 grails-gradle/plugins/src/test/resources/test-projects/no-toolchain-web/build.gradle create mode 100644 grails-gradle/plugins/src/test/resources/test-projects/no-toolchain-web/gradle.properties create mode 100644 grails-gradle/plugins/src/test/resources/test-projects/no-toolchain-web/grails-app/conf/application.yml create mode 100644 grails-gradle/plugins/src/test/resources/test-projects/no-toolchain-web/settings.gradle create mode 100644 grails-gradle/plugins/src/test/resources/test-projects/toolchain-command/build.gradle create mode 100644 grails-gradle/plugins/src/test/resources/test-projects/toolchain-command/gradle.properties create mode 100644 grails-gradle/plugins/src/test/resources/test-projects/toolchain-command/grails-app/conf/application.yml create mode 100644 grails-gradle/plugins/src/test/resources/test-projects/toolchain-command/settings.gradle create mode 100644 grails-gradle/plugins/src/test/resources/test-projects/toolchain-javaexec/build.gradle create mode 100644 grails-gradle/plugins/src/test/resources/test-projects/toolchain-javaexec/gradle.properties create mode 100644 grails-gradle/plugins/src/test/resources/test-projects/toolchain-javaexec/grails-app/conf/application.yml create mode 100644 grails-gradle/plugins/src/test/resources/test-projects/toolchain-javaexec/settings.gradle create mode 100644 grails-gradle/plugins/src/test/resources/test-projects/toolchain-override/build.gradle create mode 100644 grails-gradle/plugins/src/test/resources/test-projects/toolchain-override/gradle.properties create mode 100644 grails-gradle/plugins/src/test/resources/test-projects/toolchain-override/grails-app/conf/application.yml create mode 100644 grails-gradle/plugins/src/test/resources/test-projects/toolchain-override/settings.gradle create mode 100644 grails-gradle/plugins/src/test/resources/test-projects/toolchain-test/build.gradle create mode 100644 grails-gradle/plugins/src/test/resources/test-projects/toolchain-test/gradle.properties create mode 100644 grails-gradle/plugins/src/test/resources/test-projects/toolchain-test/grails-app/conf/application.yml create mode 100644 grails-gradle/plugins/src/test/resources/test-projects/toolchain-test/settings.gradle diff --git a/grails-gradle/plugins/build.gradle b/grails-gradle/plugins/build.gradle index e41a9cb92cf..1c397601d42 100644 --- a/grails-gradle/plugins/build.gradle +++ b/grails-gradle/plugins/build.gradle @@ -58,13 +58,14 @@ dependencies { implementation 'io.spring.gradle:dependency-management-plugin' // Testing - Gradle TestKit is auto-added by java-gradle-plugin - // NOTE: Cannot use Spock here because grails-gradle-tasks transitively - // pulls org.apache.groovy:groovy:4.0.30 which conflicts with - // org.codehaus.groovy:groovy:3.0.25 (Gradle's built-in Groovy). - // Tests use JUnit Jupiter in Java instead. - testImplementation 'org.junit.jupiter:junit-jupiter-api' + testImplementation('org.spockframework:spock-core') { transitive = false } + testImplementation 'org.codehaus.groovy:groovy-test-junit5' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' - testRuntimeOnly 'org.junit.platform:junit-platform-launcher' +} + +configurations { + testCompileClasspath.exclude group: 'org.apache.groovy', module: 'groovy' + testRuntimeClasspath.exclude group: 'org.apache.groovy', module: 'groovy' } gradlePlugin { @@ -138,6 +139,11 @@ tasks.withType(Copy) { } } +tasks.withType(Test).configureEach { + systemProperty 'projectVersion', projectVersion + systemProperty 'currentJdk', JavaVersion.current().majorVersion +} + apply { from rootProject.layout.projectDirectory.file('gradle/docs-config.gradle') from rootProject.layout.projectDirectory.file('gradle/test-config.gradle') diff --git a/grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/core/GradleSpecification.groovy b/grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/core/GradleSpecification.groovy new file mode 100644 index 00000000000..49ce68a8a29 --- /dev/null +++ b/grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/core/GradleSpecification.groovy @@ -0,0 +1,134 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.gradle.plugin.core + +import org.gradle.testkit.runner.BuildResult +import org.gradle.testkit.runner.GradleRunner +import org.gradle.testkit.runner.TaskOutcome +import spock.lang.Specification + +import java.nio.file.FileVisitResult +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.SimpleFileVisitor +import java.nio.file.attribute.BasicFileAttributes + +/** + * Base class for Gradle plugin functional tests using TestKit. + * + *

Adapted from the {@code GradleSpecification} in the + * {@code apache/grails-gradle-publish} project. Handles temp directory + * management, GradleRunner setup, test resource project copying, and + * common build assertions.

+ * + * @since 7.0.8 + */ +abstract class GradleSpecification extends Specification { + + private static Path basePath + private static GradleRunner gradleRunner + + /** Project version injected by Gradle test config. */ + protected static final String PROJECT_VERSION = System.getProperty('projectVersion') + + /** Current JDK major version injected by Gradle test config. */ + protected static final int CURRENT_JDK = Integer.parseInt(System.getProperty('currentJdk')) + + void setupSpec() { + basePath = Files.createTempDirectory('gradle-projects') + Path testKitDir = Files.createDirectories(basePath.resolve('.gradle')) + gradleRunner = GradleRunner.create() + .withPluginClasspath() + .withTestKitDir(testKitDir.toFile()) + } + + void cleanup() { + basePath?.toFile()?.listFiles()?.each { + if (it.name == '.gradle') { + return + } + it.deleteDir() + } + } + + void cleanupSpec() { + basePath?.toFile()?.deleteDir() + } + + /** + * Sets up a test project from resource files under + * {@code src/test/resources/test-projects/{projectName}}. + * + *

Files are copied to a temp directory. Any occurrence of + * {@code __CURRENT_JDK__} in {@code .gradle} files is replaced + * with the actual current JDK version, and {@code __PROJECT_VERSION__} + * is replaced with the actual project version.

+ */ + protected GradleRunner setupTestResourceProject(String projectName) { + Path destination = basePath.resolve(projectName) + Files.createDirectories(destination) + + Path source = Path.of("src/test/resources/test-projects/${projectName}") + copyDirectory(source, destination) + + gradleRunner.withProjectDir(destination.toFile()) + } + + /** + * Executes a Gradle task and returns the build result. + */ + protected BuildResult executeTask(String taskName, List otherArgs = []) { + List args = [taskName, '--stacktrace'] + args.addAll(otherArgs) + gradleRunner.withArguments(args).forwardOutput().build() + } + + /** + * Asserts that the given task succeeded. + */ + protected void assertTaskSuccess(String taskName, BuildResult result) { + def task = result.tasks.find { it.path.endsWith(":${taskName}") } + assert task != null : "Task '${taskName}' not found in build result" + assert task.outcome == TaskOutcome.SUCCESS : "Task '${taskName}' outcome was ${task.outcome}" + } + + private void copyDirectory(Path source, Path destination) { + Files.walkFileTree(source, new SimpleFileVisitor() { + @Override + FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) { + Files.createDirectories(destination.resolve(source.relativize(dir))) + FileVisitResult.CONTINUE + } + + @Override + FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { + Path target = destination.resolve(source.relativize(file)) + if (file.toString().endsWith('.gradle') || file.toString().endsWith('.properties')) { + String content = Files.readString(file) + .replace('__CURRENT_JDK__', String.valueOf(CURRENT_JDK)) + .replace('__PROJECT_VERSION__', PROJECT_VERSION) + Files.writeString(target, content) + } else { + Files.copy(file, target) + } + FileVisitResult.CONTINUE + } + }) + } +} diff --git a/grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/core/GrailsGradlePluginToolchainSpec.groovy b/grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/core/GrailsGradlePluginToolchainSpec.groovy new file mode 100644 index 00000000000..7945814c581 --- /dev/null +++ b/grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/core/GrailsGradlePluginToolchainSpec.groovy @@ -0,0 +1,136 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.gradle.plugin.core + +/** + * Tests that {@link GrailsGradlePlugin} propagates the project's Java toolchain + * to JavaExec tasks via {@code javaLauncher.convention()}. + * + *

Without the fix, JavaExec tasks spawned by Grails (dbm-* migration + * commands, console, shell, application context commands) use the JDK + * running Gradle instead of the project's configured toolchain.

+ * + * @since 7.0.8 + * @see GrailsGradlePlugin#configureToolchainForForkTasks + */ +class GrailsGradlePluginToolchainSpec extends GradleSpecification { + + // ---------------------------------------------------------------- + // Toolchain propagation + // ---------------------------------------------------------------- + + def "JavaExec tasks inherit project toolchain"() { + given: + setupTestResourceProject('toolchain-javaexec') + + when: + def result = executeTask('checkToolchain') + + then: + result.output.contains("TOOLCHAIN_VERSION=${CURRENT_JDK}") + } + + def "Test tasks inherit project toolchain"() { + given: + setupTestResourceProject('toolchain-test') + + when: + def result = executeTask('checkTestToolchain') + + then: + result.output.contains("TEST_TOOLCHAIN_VERSION=${CURRENT_JDK}") + } + + def "ApplicationContextCommandTask inherits toolchain via grails-web plugin"() { + given: + setupTestResourceProject('toolchain-command') + + when: + def result = executeTask('checkCommandToolchain') + + then: + result.output.contains("CMD_TOOLCHAIN_VERSION=${CURRENT_JDK}") + } + + def "convention allows individual task override via set()"() { + given: + setupTestResourceProject('toolchain-override') + + when: + def result = executeTask('checkOverride') + + then: + result.output.contains("OVERRIDE_VERSION=${CURRENT_JDK}") + } + + // ---------------------------------------------------------------- + // Backwards compatibility (no toolchain configured) + // ---------------------------------------------------------------- + + def "JavaExec tasks work without errors when no toolchain configured"() { + given: + setupTestResourceProject('no-toolchain-javaexec') + + when: + def result = executeTask('checkToolchain') + + then: + result.output.contains('HAS_LAUNCHER=') + } + + def "GrailsWebGradlePlugin works without errors when no toolchain configured"() { + given: + setupTestResourceProject('no-toolchain-web') + + when: + def result = executeTask('checkNoError') + + then: + result.output.contains('WEB_PLUGIN_OK=true') + } + + // ---------------------------------------------------------------- + // Fork settings preservation + // ---------------------------------------------------------------- + + def "configureForkSettings applies system properties and default heap sizes"() { + given: + setupTestResourceProject('fork-settings-defaults') + + when: + def result = executeTask('inspectSysProps') + + then: + result.output.contains('HAS_ENV=true') + result.output.contains('MIN_HEAP=768m') + result.output.contains('MAX_HEAP=768m') + } + + def "custom heap sizes are not overridden by fork settings"() { + given: + setupTestResourceProject('fork-settings-custom') + + when: + def result = executeTask('inspectHeap') + + then: + result.output.contains('MIN_HEAP=512m') + result.output.contains('MAX_HEAP=2g') + } +} diff --git a/grails-gradle/plugins/src/test/java/org/grails/gradle/plugin/core/GrailsGradlePluginToolchainTest.java b/grails-gradle/plugins/src/test/java/org/grails/gradle/plugin/core/GrailsGradlePluginToolchainTest.java deleted file mode 100644 index fe23c48abbb..00000000000 --- a/grails-gradle/plugins/src/test/java/org/grails/gradle/plugin/core/GrailsGradlePluginToolchainTest.java +++ /dev/null @@ -1,399 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.grails.gradle.plugin.core; - -import org.gradle.testkit.runner.BuildResult; -import org.gradle.testkit.runner.GradleRunner; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; - -import java.io.IOException; -import java.io.InputStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Properties; - -import static org.junit.jupiter.api.Assertions.assertTrue; - -/** - * Tests that {@link GrailsGradlePlugin} propagates the project's Java toolchain - * to JavaExec tasks via {@code javaLauncher.convention()}. - * - *

Without the fix, JavaExec tasks spawned by Grails (dbm-* migration - * commands, console, shell, application context commands) use the JDK - * running Gradle instead of the project's configured toolchain. This - * causes {@code UnsupportedClassVersionError} when the project targets - * a different JDK version than the one running Gradle.

- * - *

NOTE: Cannot use Spock here because grails-gradle-tasks transitively - * pulls {@code org.apache.groovy:groovy:4.0.30} which conflicts with - * Gradle's built-in {@code org.codehaus.groovy:groovy:3.0.25}.

- * - * @since 7.0.8 - */ -class GrailsGradlePluginToolchainTest { - - @TempDir - Path projectDir; - - private static final int CURRENT_JDK = Runtime.version().feature(); - private static final String GRAILS_VERSION = resolveGrailsVersion(); - - /** - * Reads {@code projectVersion} from the root {@code gradle.properties} so the - * test stays in sync with the actual build and is not tied to a hardcoded version. - */ - private static String resolveGrailsVersion() { - Properties props = new Properties(); - try (InputStream in = GrailsGradlePluginToolchainTest.class - .getClassLoader().getResourceAsStream("grails-gradle-plugins-project.properties")) { - if (in != null) { - props.load(in); - String v = props.getProperty("projectVersion"); - if (v != null && !v.isEmpty()) return v; - } - } catch (IOException ignored) { } - // Fallback: read from root gradle.properties relative to working directory - try { - Path root = Path.of(System.getProperty("user.dir")); - // Walk up until we find gradle.properties with projectVersion - for (Path dir = root; dir != null; dir = dir.getParent()) { - Path gp = dir.resolve("gradle.properties"); - if (Files.exists(gp)) { - Properties rootProps = new Properties(); - try (var reader = Files.newBufferedReader(gp)) { - rootProps.load(reader); - } - String v = rootProps.getProperty("projectVersion"); - if (v != null && !v.isEmpty()) return v; - } - } - } catch (IOException ignored) { } - return "7.0.8-SNAPSHOT"; - } - - @BeforeEach - void setup() throws IOException { - // Minimal Grails directory structure required by the plugin - Files.createDirectories(projectDir.resolve("grails-app/conf")); - Files.writeString(projectDir.resolve("grails-app/conf/application.yml"), ""); - Files.writeString(projectDir.resolve("settings.gradle"), "rootProject.name = 'test-toolchain'\n"); - // GrailsGradlePlugin.resolveGrailsVersion() requires this property - Files.writeString(projectDir.resolve("gradle.properties"), "grailsVersion=" + GRAILS_VERSION + "\n"); - } - - private BuildResult runBuild(String... args) { - return GradleRunner.create() - .withProjectDir(projectDir.toFile()) - .withArguments(args) - .withPluginClasspath() - .forwardOutput() - .build(); - } - - // ---------------------------------------------------------------- - // Toolchain propagation tests - // ---------------------------------------------------------------- - - @Nested - @DisplayName("With toolchain configured") - class WithToolchain { - - @Test - @DisplayName("JavaExec tasks inherit project toolchain") - void javaExecInheritsToolchain() throws IOException { - Files.writeString(projectDir.resolve("build.gradle"), - "plugins {\n" + - " id 'org.apache.grails.gradle.grails-app'\n" + - "}\n" + - "\n" + - "java {\n" + - " toolchain {\n" + - " languageVersion = JavaLanguageVersion.of(" + CURRENT_JDK + ")\n" + - " }\n" + - "}\n" + - "\n" + - "tasks.register('printLauncher', JavaExec) {\n" + - " classpath = files()\n" + - " mainClass = 'does.not.Matter'\n" + - "}\n" + - "\n" + - "tasks.register('checkToolchain') {\n" + - " doLast {\n" + - " def launcher = tasks.named('printLauncher', JavaExec).get().javaLauncher\n" + - " if (launcher.isPresent()) {\n" + - " def metadata = launcher.get().metadata\n" + - " println \"TOOLCHAIN_VERSION=${metadata.languageVersion.asInt()}\"\n" + - " } else {\n" + - " println 'TOOLCHAIN_VERSION=none'\n" + - " }\n" + - " }\n" + - "}\n" - ); - - BuildResult result = runBuild("checkToolchain", "--stacktrace"); - assertTrue(result.getOutput().contains("TOOLCHAIN_VERSION=" + CURRENT_JDK), - "JavaExec task should inherit project toolchain (JDK " + CURRENT_JDK + ")"); - } - - @Test - @DisplayName("Test tasks inherit project toolchain") - void testTaskInheritsToolchain() throws IOException { - Files.writeString(projectDir.resolve("build.gradle"), - "plugins {\n" + - " id 'org.apache.grails.gradle.grails-app'\n" + - "}\n" + - "\n" + - "java {\n" + - " toolchain {\n" + - " languageVersion = JavaLanguageVersion.of(" + CURRENT_JDK + ")\n" + - " }\n" + - "}\n" + - "\n" + - "tasks.register('checkTestToolchain') {\n" + - " doLast {\n" + - " def testTask = tasks.named('test', Test).get()\n" + - " def launcher = testTask.javaLauncher\n" + - " if (launcher.isPresent()) {\n" + - " println \"TEST_TOOLCHAIN_VERSION=${launcher.get().metadata.languageVersion.asInt()}\"\n" + - " } else {\n" + - " println 'TEST_TOOLCHAIN_VERSION=none'\n" + - " }\n" + - " }\n" + - "}\n" - ); - - BuildResult result = runBuild("checkTestToolchain", "--stacktrace"); - assertTrue(result.getOutput().contains("TEST_TOOLCHAIN_VERSION=" + CURRENT_JDK), - "Test task should inherit project toolchain (JDK " + CURRENT_JDK + ")"); - } - - @Test - @DisplayName("ApplicationContextCommandTask inherits toolchain via grails-web plugin") - void applicationContextCommandTaskInheritsToolchain() throws IOException { - Files.writeString(projectDir.resolve("build.gradle"), - "plugins {\n" + - " id 'org.apache.grails.gradle.grails-web'\n" + - "}\n" + - "\n" + - "java {\n" + - " toolchain {\n" + - " languageVersion = JavaLanguageVersion.of(" + CURRENT_JDK + ")\n" + - " }\n" + - "}\n" + - "\n" + - "tasks.register('checkCommandToolchain') {\n" + - " doLast {\n" + - " def cmdTask = tasks.named('urlMappingsReport').get()\n" + - " if (cmdTask instanceof JavaExec) {\n" + - " def launcher = ((JavaExec) cmdTask).javaLauncher\n" + - " if (launcher.isPresent()) {\n" + - " println \"CMD_TOOLCHAIN_VERSION=${launcher.get().metadata.languageVersion.asInt()}\"\n" + - " } else {\n" + - " println 'CMD_TOOLCHAIN_VERSION=none'\n" + - " }\n" + - " } else {\n" + - " println 'CMD_TOOLCHAIN_VERSION=not_javaexec'\n" + - " }\n" + - " }\n" + - "}\n" - ); - - BuildResult result = runBuild("checkCommandToolchain", "--stacktrace"); - assertTrue(result.getOutput().contains("CMD_TOOLCHAIN_VERSION=" + CURRENT_JDK), - "ApplicationContextCommandTask should inherit project toolchain (JDK " + CURRENT_JDK + ")"); - } - - @Test - @DisplayName("convention() allows individual task override via set()") - void conventionAllowsOverride() throws IOException { - Files.writeString(projectDir.resolve("build.gradle"), - "import org.gradle.jvm.toolchain.JavaLanguageVersion\n" + - "\n" + - "plugins {\n" + - " id 'org.apache.grails.gradle.grails-app'\n" + - "}\n" + - "\n" + - "java {\n" + - " toolchain {\n" + - " languageVersion = JavaLanguageVersion.of(" + CURRENT_JDK + ")\n" + - " }\n" + - "}\n" + - "\n" + - "tasks.register('customExec', JavaExec) {\n" + - " classpath = files()\n" + - " mainClass = 'does.not.Matter'\n" + - " javaLauncher.set(javaToolchains.launcherFor {\n" + - " languageVersion = JavaLanguageVersion.of(" + CURRENT_JDK + ")\n" + - " })\n" + - "}\n" + - "\n" + - "tasks.register('checkOverride') {\n" + - " doLast {\n" + - " def launcher = tasks.named('customExec', JavaExec).get().javaLauncher\n" + - " if (launcher.isPresent()) {\n" + - " println \"OVERRIDE_VERSION=${launcher.get().metadata.languageVersion.asInt()}\"\n" + - " } else {\n" + - " println 'OVERRIDE_VERSION=none'\n" + - " }\n" + - " }\n" + - "}\n" - ); - - BuildResult result = runBuild("checkOverride", "--stacktrace"); - assertTrue(result.getOutput().contains("OVERRIDE_VERSION=" + CURRENT_JDK), - "Task with explicit set() should override convention"); - } - } - - // ---------------------------------------------------------------- - // No-toolchain backwards compatibility tests - // ---------------------------------------------------------------- - - @Nested - @DisplayName("Without toolchain configured (backwards compatibility)") - class WithoutToolchain { - - @Test - @DisplayName("JavaExec tasks work without errors when no toolchain configured") - void javaExecWorksWithoutToolchain() throws IOException { - Files.writeString(projectDir.resolve("build.gradle"), - "plugins {\n" + - " id 'org.apache.grails.gradle.grails-app'\n" + - "}\n" + - "\n" + - "tasks.register('printLauncher', JavaExec) {\n" + - " classpath = files()\n" + - " mainClass = 'does.not.Matter'\n" + - "}\n" + - "\n" + - "tasks.register('checkToolchain') {\n" + - " doLast {\n" + - " def launcher = tasks.named('printLauncher', JavaExec).get().javaLauncher\n" + - " if (launcher.isPresent()) {\n" + - " println 'HAS_LAUNCHER=true'\n" + - " } else {\n" + - " println 'HAS_LAUNCHER=false'\n" + - " }\n" + - " }\n" + - "}\n" - ); - - BuildResult result = runBuild("checkToolchain", "--stacktrace"); - assertTrue(result.getOutput().contains("HAS_LAUNCHER="), - "Plugin should not error when no toolchain is configured"); - } - - @Test - @DisplayName("GrailsWebGradlePlugin works without errors when no toolchain configured") - void webPluginWorksWithoutToolchain() throws IOException { - Files.writeString(projectDir.resolve("build.gradle"), - "plugins {\n" + - " id 'org.apache.grails.gradle.grails-web'\n" + - "}\n" + - "\n" + - "tasks.register('checkNoError') {\n" + - " doLast {\n" + - " println 'WEB_PLUGIN_OK=true'\n" + - " }\n" + - "}\n" - ); - - BuildResult result = runBuild("checkNoError", "--stacktrace"); - assertTrue(result.getOutput().contains("WEB_PLUGIN_OK=true"), - "GrailsWebGradlePlugin should work without toolchain configured"); - } - } - - // ---------------------------------------------------------------- - // Fork settings tests (system properties, heap sizes) - // ---------------------------------------------------------------- - - @Nested - @DisplayName("Fork settings (existing behavior preserved)") - class ForkSettings { - - @Test - @DisplayName("configureForkSettings applies system properties and default heap sizes") - void forkSettingsApplyDefaults() throws IOException { - Files.writeString(projectDir.resolve("build.gradle"), - "plugins {\n" + - " id 'org.apache.grails.gradle.grails-app'\n" + - "}\n" + - "\n" + - "tasks.register('checkSysProps', JavaExec) {\n" + - " classpath = files()\n" + - " mainClass = 'does.not.Matter'\n" + - "}\n" + - "\n" + - "tasks.register('inspectSysProps') {\n" + - " doLast {\n" + - " def task = tasks.named('checkSysProps', JavaExec).get()\n" + - " def sysProps = task.systemProperties\n" + - " println \"HAS_ENV=${sysProps.containsKey('grails.env')}\"\n" + - " println \"MIN_HEAP=${task.minHeapSize}\"\n" + - " println \"MAX_HEAP=${task.maxHeapSize}\"\n" + - " }\n" + - "}\n" - ); - - BuildResult result = runBuild("inspectSysProps", "--stacktrace"); - assertTrue(result.getOutput().contains("HAS_ENV=true"), - "Fork settings should set grails.env system property"); - assertTrue(result.getOutput().contains("MIN_HEAP=768m"), - "Default min heap should be 768m"); - assertTrue(result.getOutput().contains("MAX_HEAP=768m"), - "Default max heap should be 768m"); - } - - @Test - @DisplayName("Custom heap sizes are not overridden by fork settings") - void customHeapSizesPreserved() throws IOException { - Files.writeString(projectDir.resolve("build.gradle"), - "plugins {\n" + - " id 'org.apache.grails.gradle.grails-app'\n" + - "}\n" + - "\n" + - "tasks.register('customHeap', JavaExec) {\n" + - " classpath = files()\n" + - " mainClass = 'does.not.Matter'\n" + - " minHeapSize = '512m'\n" + - " maxHeapSize = '2g'\n" + - "}\n" + - "\n" + - "tasks.register('inspectHeap') {\n" + - " doLast {\n" + - " def task = tasks.named('customHeap', JavaExec).get()\n" + - " println \"MIN_HEAP=${task.minHeapSize}\"\n" + - " println \"MAX_HEAP=${task.maxHeapSize}\"\n" + - " }\n" + - "}\n" - ); - - BuildResult result = runBuild("inspectHeap", "--stacktrace"); - assertTrue(result.getOutput().contains("MIN_HEAP=512m"), - "Custom min heap should be preserved"); - assertTrue(result.getOutput().contains("MAX_HEAP=2g"), - "Custom max heap should be preserved"); - } - } -} diff --git a/grails-gradle/plugins/src/test/resources/test-projects/fork-settings-custom/build.gradle b/grails-gradle/plugins/src/test/resources/test-projects/fork-settings-custom/build.gradle new file mode 100644 index 00000000000..8e9ffc40181 --- /dev/null +++ b/grails-gradle/plugins/src/test/resources/test-projects/fork-settings-custom/build.gradle @@ -0,0 +1,18 @@ +plugins { + id 'org.apache.grails.gradle.grails-app' +} + +tasks.register('customHeap', JavaExec) { + classpath = files() + mainClass = 'does.not.Matter' + minHeapSize = '512m' + maxHeapSize = '2g' +} + +tasks.register('inspectHeap') { + doLast { + def task = tasks.named('customHeap', JavaExec).get() + println "MIN_HEAP=${task.minHeapSize}" + println "MAX_HEAP=${task.maxHeapSize}" + } +} diff --git a/grails-gradle/plugins/src/test/resources/test-projects/fork-settings-custom/gradle.properties b/grails-gradle/plugins/src/test/resources/test-projects/fork-settings-custom/gradle.properties new file mode 100644 index 00000000000..35c332fb874 --- /dev/null +++ b/grails-gradle/plugins/src/test/resources/test-projects/fork-settings-custom/gradle.properties @@ -0,0 +1 @@ +grailsVersion=__PROJECT_VERSION__ diff --git a/grails-gradle/plugins/src/test/resources/test-projects/fork-settings-custom/grails-app/conf/application.yml b/grails-gradle/plugins/src/test/resources/test-projects/fork-settings-custom/grails-app/conf/application.yml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/grails-gradle/plugins/src/test/resources/test-projects/fork-settings-custom/settings.gradle b/grails-gradle/plugins/src/test/resources/test-projects/fork-settings-custom/settings.gradle new file mode 100644 index 00000000000..c426fa21f91 --- /dev/null +++ b/grails-gradle/plugins/src/test/resources/test-projects/fork-settings-custom/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'test-toolchain' diff --git a/grails-gradle/plugins/src/test/resources/test-projects/fork-settings-defaults/build.gradle b/grails-gradle/plugins/src/test/resources/test-projects/fork-settings-defaults/build.gradle new file mode 100644 index 00000000000..9816aab19d4 --- /dev/null +++ b/grails-gradle/plugins/src/test/resources/test-projects/fork-settings-defaults/build.gradle @@ -0,0 +1,18 @@ +plugins { + id 'org.apache.grails.gradle.grails-app' +} + +tasks.register('checkSysProps', JavaExec) { + classpath = files() + mainClass = 'does.not.Matter' +} + +tasks.register('inspectSysProps') { + doLast { + def task = tasks.named('checkSysProps', JavaExec).get() + def sysProps = task.systemProperties + println "HAS_ENV=${sysProps.containsKey('grails.env')}" + println "MIN_HEAP=${task.minHeapSize}" + println "MAX_HEAP=${task.maxHeapSize}" + } +} diff --git a/grails-gradle/plugins/src/test/resources/test-projects/fork-settings-defaults/gradle.properties b/grails-gradle/plugins/src/test/resources/test-projects/fork-settings-defaults/gradle.properties new file mode 100644 index 00000000000..35c332fb874 --- /dev/null +++ b/grails-gradle/plugins/src/test/resources/test-projects/fork-settings-defaults/gradle.properties @@ -0,0 +1 @@ +grailsVersion=__PROJECT_VERSION__ diff --git a/grails-gradle/plugins/src/test/resources/test-projects/fork-settings-defaults/grails-app/conf/application.yml b/grails-gradle/plugins/src/test/resources/test-projects/fork-settings-defaults/grails-app/conf/application.yml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/grails-gradle/plugins/src/test/resources/test-projects/fork-settings-defaults/settings.gradle b/grails-gradle/plugins/src/test/resources/test-projects/fork-settings-defaults/settings.gradle new file mode 100644 index 00000000000..c426fa21f91 --- /dev/null +++ b/grails-gradle/plugins/src/test/resources/test-projects/fork-settings-defaults/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'test-toolchain' diff --git a/grails-gradle/plugins/src/test/resources/test-projects/no-toolchain-javaexec/build.gradle b/grails-gradle/plugins/src/test/resources/test-projects/no-toolchain-javaexec/build.gradle new file mode 100644 index 00000000000..fafaed9db77 --- /dev/null +++ b/grails-gradle/plugins/src/test/resources/test-projects/no-toolchain-javaexec/build.gradle @@ -0,0 +1,19 @@ +plugins { + id 'org.apache.grails.gradle.grails-app' +} + +tasks.register('printLauncher', JavaExec) { + classpath = files() + mainClass = 'does.not.Matter' +} + +tasks.register('checkToolchain') { + doLast { + def launcher = tasks.named('printLauncher', JavaExec).get().javaLauncher + if (launcher.isPresent()) { + println 'HAS_LAUNCHER=true' + } else { + println 'HAS_LAUNCHER=false' + } + } +} diff --git a/grails-gradle/plugins/src/test/resources/test-projects/no-toolchain-javaexec/gradle.properties b/grails-gradle/plugins/src/test/resources/test-projects/no-toolchain-javaexec/gradle.properties new file mode 100644 index 00000000000..35c332fb874 --- /dev/null +++ b/grails-gradle/plugins/src/test/resources/test-projects/no-toolchain-javaexec/gradle.properties @@ -0,0 +1 @@ +grailsVersion=__PROJECT_VERSION__ diff --git a/grails-gradle/plugins/src/test/resources/test-projects/no-toolchain-javaexec/grails-app/conf/application.yml b/grails-gradle/plugins/src/test/resources/test-projects/no-toolchain-javaexec/grails-app/conf/application.yml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/grails-gradle/plugins/src/test/resources/test-projects/no-toolchain-javaexec/settings.gradle b/grails-gradle/plugins/src/test/resources/test-projects/no-toolchain-javaexec/settings.gradle new file mode 100644 index 00000000000..c426fa21f91 --- /dev/null +++ b/grails-gradle/plugins/src/test/resources/test-projects/no-toolchain-javaexec/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'test-toolchain' diff --git a/grails-gradle/plugins/src/test/resources/test-projects/no-toolchain-web/build.gradle b/grails-gradle/plugins/src/test/resources/test-projects/no-toolchain-web/build.gradle new file mode 100644 index 00000000000..60e07b18708 --- /dev/null +++ b/grails-gradle/plugins/src/test/resources/test-projects/no-toolchain-web/build.gradle @@ -0,0 +1,9 @@ +plugins { + id 'org.apache.grails.gradle.grails-web' +} + +tasks.register('checkNoError') { + doLast { + println 'WEB_PLUGIN_OK=true' + } +} diff --git a/grails-gradle/plugins/src/test/resources/test-projects/no-toolchain-web/gradle.properties b/grails-gradle/plugins/src/test/resources/test-projects/no-toolchain-web/gradle.properties new file mode 100644 index 00000000000..35c332fb874 --- /dev/null +++ b/grails-gradle/plugins/src/test/resources/test-projects/no-toolchain-web/gradle.properties @@ -0,0 +1 @@ +grailsVersion=__PROJECT_VERSION__ diff --git a/grails-gradle/plugins/src/test/resources/test-projects/no-toolchain-web/grails-app/conf/application.yml b/grails-gradle/plugins/src/test/resources/test-projects/no-toolchain-web/grails-app/conf/application.yml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/grails-gradle/plugins/src/test/resources/test-projects/no-toolchain-web/settings.gradle b/grails-gradle/plugins/src/test/resources/test-projects/no-toolchain-web/settings.gradle new file mode 100644 index 00000000000..c426fa21f91 --- /dev/null +++ b/grails-gradle/plugins/src/test/resources/test-projects/no-toolchain-web/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'test-toolchain' diff --git a/grails-gradle/plugins/src/test/resources/test-projects/toolchain-command/build.gradle b/grails-gradle/plugins/src/test/resources/test-projects/toolchain-command/build.gradle new file mode 100644 index 00000000000..12222d08e39 --- /dev/null +++ b/grails-gradle/plugins/src/test/resources/test-projects/toolchain-command/build.gradle @@ -0,0 +1,25 @@ +plugins { + id 'org.apache.grails.gradle.grails-web' +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(__CURRENT_JDK__) + } +} + +tasks.register('checkCommandToolchain') { + doLast { + def cmdTask = tasks.named('urlMappingsReport').get() + if (cmdTask instanceof JavaExec) { + def launcher = ((JavaExec) cmdTask).javaLauncher + if (launcher.isPresent()) { + println "CMD_TOOLCHAIN_VERSION=${launcher.get().metadata.languageVersion.asInt()}" + } else { + println 'CMD_TOOLCHAIN_VERSION=none' + } + } else { + println 'CMD_TOOLCHAIN_VERSION=not_javaexec' + } + } +} diff --git a/grails-gradle/plugins/src/test/resources/test-projects/toolchain-command/gradle.properties b/grails-gradle/plugins/src/test/resources/test-projects/toolchain-command/gradle.properties new file mode 100644 index 00000000000..35c332fb874 --- /dev/null +++ b/grails-gradle/plugins/src/test/resources/test-projects/toolchain-command/gradle.properties @@ -0,0 +1 @@ +grailsVersion=__PROJECT_VERSION__ diff --git a/grails-gradle/plugins/src/test/resources/test-projects/toolchain-command/grails-app/conf/application.yml b/grails-gradle/plugins/src/test/resources/test-projects/toolchain-command/grails-app/conf/application.yml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/grails-gradle/plugins/src/test/resources/test-projects/toolchain-command/settings.gradle b/grails-gradle/plugins/src/test/resources/test-projects/toolchain-command/settings.gradle new file mode 100644 index 00000000000..c426fa21f91 --- /dev/null +++ b/grails-gradle/plugins/src/test/resources/test-projects/toolchain-command/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'test-toolchain' diff --git a/grails-gradle/plugins/src/test/resources/test-projects/toolchain-javaexec/build.gradle b/grails-gradle/plugins/src/test/resources/test-projects/toolchain-javaexec/build.gradle new file mode 100644 index 00000000000..bc4c3757ec2 --- /dev/null +++ b/grails-gradle/plugins/src/test/resources/test-projects/toolchain-javaexec/build.gradle @@ -0,0 +1,26 @@ +plugins { + id 'org.apache.grails.gradle.grails-app' +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(__CURRENT_JDK__) + } +} + +tasks.register('printLauncher', JavaExec) { + classpath = files() + mainClass = 'does.not.Matter' +} + +tasks.register('checkToolchain') { + doLast { + def launcher = tasks.named('printLauncher', JavaExec).get().javaLauncher + if (launcher.isPresent()) { + def metadata = launcher.get().metadata + println "TOOLCHAIN_VERSION=${metadata.languageVersion.asInt()}" + } else { + println 'TOOLCHAIN_VERSION=none' + } + } +} diff --git a/grails-gradle/plugins/src/test/resources/test-projects/toolchain-javaexec/gradle.properties b/grails-gradle/plugins/src/test/resources/test-projects/toolchain-javaexec/gradle.properties new file mode 100644 index 00000000000..35c332fb874 --- /dev/null +++ b/grails-gradle/plugins/src/test/resources/test-projects/toolchain-javaexec/gradle.properties @@ -0,0 +1 @@ +grailsVersion=__PROJECT_VERSION__ diff --git a/grails-gradle/plugins/src/test/resources/test-projects/toolchain-javaexec/grails-app/conf/application.yml b/grails-gradle/plugins/src/test/resources/test-projects/toolchain-javaexec/grails-app/conf/application.yml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/grails-gradle/plugins/src/test/resources/test-projects/toolchain-javaexec/settings.gradle b/grails-gradle/plugins/src/test/resources/test-projects/toolchain-javaexec/settings.gradle new file mode 100644 index 00000000000..c426fa21f91 --- /dev/null +++ b/grails-gradle/plugins/src/test/resources/test-projects/toolchain-javaexec/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'test-toolchain' diff --git a/grails-gradle/plugins/src/test/resources/test-projects/toolchain-override/build.gradle b/grails-gradle/plugins/src/test/resources/test-projects/toolchain-override/build.gradle new file mode 100644 index 00000000000..a70d289314c --- /dev/null +++ b/grails-gradle/plugins/src/test/resources/test-projects/toolchain-override/build.gradle @@ -0,0 +1,30 @@ +import org.gradle.jvm.toolchain.JavaLanguageVersion + +plugins { + id 'org.apache.grails.gradle.grails-app' +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(__CURRENT_JDK__) + } +} + +tasks.register('customExec', JavaExec) { + classpath = files() + mainClass = 'does.not.Matter' + javaLauncher.set(javaToolchains.launcherFor { + languageVersion = JavaLanguageVersion.of(__CURRENT_JDK__) + }) +} + +tasks.register('checkOverride') { + doLast { + def launcher = tasks.named('customExec', JavaExec).get().javaLauncher + if (launcher.isPresent()) { + println "OVERRIDE_VERSION=${launcher.get().metadata.languageVersion.asInt()}" + } else { + println 'OVERRIDE_VERSION=none' + } + } +} diff --git a/grails-gradle/plugins/src/test/resources/test-projects/toolchain-override/gradle.properties b/grails-gradle/plugins/src/test/resources/test-projects/toolchain-override/gradle.properties new file mode 100644 index 00000000000..35c332fb874 --- /dev/null +++ b/grails-gradle/plugins/src/test/resources/test-projects/toolchain-override/gradle.properties @@ -0,0 +1 @@ +grailsVersion=__PROJECT_VERSION__ diff --git a/grails-gradle/plugins/src/test/resources/test-projects/toolchain-override/grails-app/conf/application.yml b/grails-gradle/plugins/src/test/resources/test-projects/toolchain-override/grails-app/conf/application.yml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/grails-gradle/plugins/src/test/resources/test-projects/toolchain-override/settings.gradle b/grails-gradle/plugins/src/test/resources/test-projects/toolchain-override/settings.gradle new file mode 100644 index 00000000000..c426fa21f91 --- /dev/null +++ b/grails-gradle/plugins/src/test/resources/test-projects/toolchain-override/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'test-toolchain' diff --git a/grails-gradle/plugins/src/test/resources/test-projects/toolchain-test/build.gradle b/grails-gradle/plugins/src/test/resources/test-projects/toolchain-test/build.gradle new file mode 100644 index 00000000000..e397472e6de --- /dev/null +++ b/grails-gradle/plugins/src/test/resources/test-projects/toolchain-test/build.gradle @@ -0,0 +1,21 @@ +plugins { + id 'org.apache.grails.gradle.grails-app' +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(__CURRENT_JDK__) + } +} + +tasks.register('checkTestToolchain') { + doLast { + def testTask = tasks.named('test', Test).get() + def launcher = testTask.javaLauncher + if (launcher.isPresent()) { + println "TEST_TOOLCHAIN_VERSION=${launcher.get().metadata.languageVersion.asInt()}" + } else { + println 'TEST_TOOLCHAIN_VERSION=none' + } + } +} diff --git a/grails-gradle/plugins/src/test/resources/test-projects/toolchain-test/gradle.properties b/grails-gradle/plugins/src/test/resources/test-projects/toolchain-test/gradle.properties new file mode 100644 index 00000000000..35c332fb874 --- /dev/null +++ b/grails-gradle/plugins/src/test/resources/test-projects/toolchain-test/gradle.properties @@ -0,0 +1 @@ +grailsVersion=__PROJECT_VERSION__ diff --git a/grails-gradle/plugins/src/test/resources/test-projects/toolchain-test/grails-app/conf/application.yml b/grails-gradle/plugins/src/test/resources/test-projects/toolchain-test/grails-app/conf/application.yml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/grails-gradle/plugins/src/test/resources/test-projects/toolchain-test/settings.gradle b/grails-gradle/plugins/src/test/resources/test-projects/toolchain-test/settings.gradle new file mode 100644 index 00000000000..c426fa21f91 --- /dev/null +++ b/grails-gradle/plugins/src/test/resources/test-projects/toolchain-test/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'test-toolchain' From 5d7c861a40438aa7c2f9d9a665cc413010cd2327 Mon Sep 17 00:00:00 2001 From: James Fredley Date: Thu, 19 Feb 2026 10:29:46 -0500 Subject: [PATCH 3/3] fix: move test system properties to shared test-config.gradle Move projectVersion and currentJdk system properties from the plugins build.gradle into the shared test-config.gradle for consistency across all grails-gradle modules. Assisted-by: Claude Code --- grails-gradle/gradle/test-config.gradle | 2 ++ grails-gradle/plugins/build.gradle | 5 ----- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/grails-gradle/gradle/test-config.gradle b/grails-gradle/gradle/test-config.gradle index 78abf85e4d7..8f166e00978 100644 --- a/grails-gradle/gradle/test-config.gradle +++ b/grails-gradle/gradle/test-config.gradle @@ -40,6 +40,8 @@ tasks.withType(Test).configureEach { } useJUnitPlatform() + systemProperty 'projectVersion', projectVersion + systemProperty 'currentJdk', JavaVersion.current().majorVersion jvmArgs += java17moduleReflectionCompatibilityArguments testLogging { events('passed', 'skipped', 'failed') diff --git a/grails-gradle/plugins/build.gradle b/grails-gradle/plugins/build.gradle index 1c397601d42..cedf995b7aa 100644 --- a/grails-gradle/plugins/build.gradle +++ b/grails-gradle/plugins/build.gradle @@ -139,11 +139,6 @@ tasks.withType(Copy) { } } -tasks.withType(Test).configureEach { - systemProperty 'projectVersion', projectVersion - systemProperty 'currentJdk', JavaVersion.current().majorVersion -} - apply { from rootProject.layout.projectDirectory.file('gradle/docs-config.gradle') from rootProject.layout.projectDirectory.file('gradle/test-config.gradle')