Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 127 additions & 0 deletions buildSrc/src/main/kotlin/datadog.test-jvm-contraints.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import datadog.gradle.plugin.testJvmConstraints.ProvideJvmArgsOnJvmLauncherVersion
import datadog.gradle.plugin.testJvmConstraints.TestJvmConstraintsExtension
import datadog.gradle.plugin.testJvmConstraints.TestJvmSpec
import datadog.gradle.plugin.testJvmConstraints.isJavaVersionAllowed
import datadog.gradle.plugin.testJvmConstraints.isTestJvmAllowed

plugins {
java
}

val projectExtension = extensions.create<TestJvmConstraintsExtension>(TestJvmConstraintsExtension.NAME)

val testJvmSpec = TestJvmSpec(project)

tasks.withType<Test>().configureEach {
if (extensions.findByName(TestJvmConstraintsExtension.NAME) != null) {
return@configureEach
}

inputs.property("testJvm", testJvmSpec.testJvmProperty).optional(true)

val taskExtension = project.objects.newInstance<TestJvmConstraintsExtension>().also {
configureConventions(it, projectExtension)
}

inputs.property("${TestJvmConstraintsExtension.NAME}.allowReflectiveAccessToJdk", taskExtension.allowReflectiveAccessToJdk).optional(true)
inputs.property("${TestJvmConstraintsExtension.NAME}.excludeJdk", taskExtension.excludeJdk)
inputs.property("${TestJvmConstraintsExtension.NAME}.includeJdk", taskExtension.includeJdk)
inputs.property("${TestJvmConstraintsExtension.NAME}.forceJdk", taskExtension.forceJdk)
inputs.property("${TestJvmConstraintsExtension.NAME}.minJavaVersion", taskExtension.minJavaVersion).optional(true)
inputs.property("${TestJvmConstraintsExtension.NAME}.maxJavaVersion", taskExtension.maxJavaVersion).optional(true)

extensions.add(TestJvmConstraintsExtension.NAME, taskExtension)
Comment on lines +26 to +33
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would it make sense to extract TestJvmConstraintsExtension.NAME to local var?


configureTestJvm(taskExtension)
}

/**
* Provide arguments if condition is met.
*/
fun Test.conditionalJvmArgs(
applyFromVersion: JavaVersion,
jvmArgsToApply: List<String>,
additionalCondition: Provider<Boolean> = project.providers.provider { true }
) {
jvmArgumentProviders.add(
ProvideJvmArgsOnJvmLauncherVersion(
this,
applyFromVersion,
jvmArgsToApply,
additionalCondition
)
)
}

/**
* Configure the jvm launcher of the test task and ensure the test task
* can be run with the test task launcher.
*/
private fun Test.configureTestJvm(extension: TestJvmConstraintsExtension) {
if (testJvmSpec.javaTestLauncher.isPresent) {
javaLauncher = testJvmSpec.javaTestLauncher
onlyIf("Allowed or forced JDK") {
extension.isTestJvmAllowed(testJvmSpec)
}
} else {
onlyIf("Is current Daemon JVM allowed") {
extension.isJavaVersionAllowed(JavaVersion.current())
}
}

// temporary workaround when using Java16+: some tests require reflective access to java.lang/java.util
conditionalJvmArgs(
JavaVersion.VERSION_16,
listOf(
"--add-opens=java.base/java.lang=ALL-UNNAMED",
"--add-opens=java.base/java.util=ALL-UNNAMED"
),
extension.allowReflectiveAccessToJdk
)
}

// Jacoco plugin is not applied on every project
pluginManager.withPlugin("org.gradle.jacoco") {
tasks.withType<Test>().configureEach {
// Disable jacoco for additional 'testJvm' tests to speed things up a bit
if (testJvmSpec.javaTestLauncher.isPresent) {
extensions.configure<JacocoTaskExtension> {
isEnabled = false
}
}
}
}

/**
* Configures the convention, this tells Gradle where to look for values.
*
* Currently, the extension is still configured to look at project's _extra_ properties.
*/
private fun Test.configureConventions(
taskExtension: TestJvmConstraintsExtension,
projectExtension: TestJvmConstraintsExtension
) {
taskExtension.minJavaVersion.convention(projectExtension.minJavaVersion
.orElse(providers.provider { project.findProperty("${name}MinJavaVersionForTests") as? JavaVersion })
.orElse(providers.provider { project.findProperty("minJavaVersion") as? JavaVersion })
)
taskExtension.maxJavaVersion.convention(projectExtension.maxJavaVersion
.orElse(providers.provider { project.findProperty("${name}MaxJavaVersionForTests") as? JavaVersion })
.orElse(providers.provider { project.findProperty("maxJavaVersion") as? JavaVersion })
)
taskExtension.forceJdk.convention(projectExtension.forceJdk
.orElse(providers.provider {
@Suppress("UNCHECKED_CAST")
project.findProperty("forceJdk") as? List<String> ?: emptyList()
})
)
taskExtension.excludeJdk.convention(projectExtension.excludeJdk
.orElse(providers.provider {
@Suppress("UNCHECKED_CAST")
project.findProperty("excludeJdk") as? List<String> ?: emptyList()
})
)
taskExtension.allowReflectiveAccessToJdk.convention(projectExtension.allowReflectiveAccessToJdk
.orElse(providers.provider { project.findProperty("allowReflectiveAccessToJdk") as? Boolean })
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package datadog.gradle.plugin.testJvmConstraints

import org.gradle.api.JavaVersion
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.testing.Test
import org.gradle.process.CommandLineArgumentProvider

class ProvideJvmArgsOnJvmLauncherVersion(
@get:Internal
val test: Test,

@get:Input
val applyFromVersion: JavaVersion,

@get:Input
val jvmArgsToApply: List<String>,

@get:Input
@get:Optional
val additionalCondition: Provider<Boolean>
) : CommandLineArgumentProvider {

override fun asArguments(): Iterable<String> {
val launcherVersion = test.javaLauncher
.map { JavaVersion.toVersion(it.metadata.languageVersion.asInt()) }
.orElse(JavaVersion.current())
.get()

return if (launcherVersion.isCompatibleWith(applyFromVersion) && additionalCondition.getOrElse(true)) {
jvmArgsToApply
} else {
emptyList()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package datadog.gradle.plugin.testJvmConstraints

import org.gradle.api.JavaVersion
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property

interface TestJvmConstraintsExtension {
/**
* Sets an explicit minimum bound to allowed JDK version
*/
val minJavaVersion: Property<JavaVersion>

/**
* Sets an explicit maximum bound to allowed JDK version
*/
val maxJavaVersion: Property<JavaVersion>

/**
* List of allowed JDK names (passed through the `testJvm` property).
*/
val forceJdk: ListProperty<String>

/**
* List of included JDK names (passed through the `testJvm` property).
*/
val includeJdk: ListProperty<String>

/**
* List of excluded JDK names (passed through the `testJvm` property).
*/
val excludeJdk: ListProperty<String>

/**
* Indicate if test jvm allows reflective access to JDK modules, in particular this toggle
* openning `java.base/java.lang` and `java.base/java.util`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* openning `java.base/java.lang` and `java.base/java.util`.
* allows usage of `java.base/java.lang` and `java.base/java.util`.

Does this make sense?

*/
val allowReflectiveAccessToJdk: Property<Boolean>

companion object {
const val NAME = "testJvmConstraint"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package datadog.gradle.plugin.testJvmConstraints

import org.gradle.api.JavaVersion
import org.gradle.api.logging.Logging

private val logger = Logging.getLogger("TestJvmConstraintsUtils")

internal fun TestJvmConstraintsExtension.isJavaVersionAllowed(version: JavaVersion): Boolean {
return isWithinAllowedRange(version)
}

internal fun TestJvmConstraintsExtension.isTestJvmAllowed(testJvmSpec: TestJvmSpec): Boolean {
val testJvmName = testJvmSpec.normalizedTestJvm.get()

val included = includeJdk.get()
if (included.isNotEmpty() && included.none { it.equals(testJvmName, ignoreCase = true) }) {
return false
}

val excluded = excludeJdk.get()
if (excluded.isNotEmpty() && excluded.any { it.equals(testJvmName, ignoreCase = true) }) {
return false
}

val launcherVersion = JavaVersion.toVersion(testJvmSpec.javaTestLauncher.get().metadata.languageVersion.asInt())
if (!isWithinAllowedRange(launcherVersion) && forceJdk.get().none { it.equals(testJvmName, ignoreCase = true) }) {
return false
}

return true
}

private fun TestJvmConstraintsExtension.isWithinAllowedRange(currentJvmVersion: JavaVersion): Boolean {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: rename isWithinAllowedRange --> withinAllowedRange.

val definedMin = minJavaVersion.isPresent
val definedMax = maxJavaVersion.isPresent

if (definedMin && (minJavaVersion.get()) > currentJvmVersion) {
logger.info("isWithinAllowedRange returns false b/o minProp=${minJavaVersion.get()} is defined and greater than version=$currentJvmVersion")
return false
}

if (definedMax && (maxJavaVersion.get()) < currentJvmVersion) {
logger.info("isWithinAllowedRange returns false b/o maxProp=${maxJavaVersion.get()} is defined and lower than version=$currentJvmVersion")
return false
}

return true
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package datadog.gradle.plugin.testJvmConstraints

import org.gradle.kotlin.dsl.support.serviceOf
import org.gradle.api.GradleException
import org.gradle.api.Project
import org.gradle.jvm.toolchain.JavaLauncher
import org.gradle.jvm.toolchain.JavaToolchainService
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths


class TestJvmSpec(val project: Project) {
companion object {
const val TEST_JVM = "testJvm"
}

private val currentJavaHomePath = project.providers.systemProperty("java.home").map { it.normalizeToJDKJavaHome() }

val testJvmProperty = project.providers.gradleProperty(TEST_JVM)

val normalizedTestJvm = testJvmProperty.map { testJvm ->
if (testJvm.isBlank()) {
throw GradleException("testJvm property is blank")
}

// "stable" is calculated as the largest X found in JAVA_X_HOME
if (testJvm == "stable") {
val javaVersions = project.providers.environmentVariablesPrefixedBy("JAVA_").map { javaHomes ->
javaHomes
.filter { it.key.matches(Regex("^JAVA_[0-9]+_HOME$")) }
.map { Regex("^JAVA_(\\d+)_HOME$").find(it.key)!!.groupValues[1].toInt() }
}.get()

if (javaVersions.isEmpty()) {
throw GradleException("No valid JAVA_X_HOME environment variables found.")
}

javaVersions.max().toString()
} else {
testJvm
}
}.map { project.logger.info("normalized testJvm: $it"); it }

val testJvmHomePath = normalizedTestJvm.map {
if (Files.exists(Paths.get(it))) {
it.normalizeToJDKJavaHome()
} else {
val matcher = Regex("([a-zA-Z]*)([0-9]+)").find(it)
if (matcher == null) {
throw GradleException("Unable to find launcher for Java '$it'. It needs to match '([a-zA-Z]*)([0-9]+)'.")
}
val testJvmEnv = "JAVA_${it}_HOME"
val testJvmHome = project.providers.environmentVariable(testJvmEnv).orNull
if (testJvmHome == null) {
throw GradleException("Unable to find launcher for Java '$it'. Have you set '$testJvmEnv'?")
}

testJvmHome.normalizeToJDKJavaHome()
}
}.map { project.logger.info("testJvm home path: $it"); it }

val javaTestLauncher = project.providers.zip(testJvmHomePath, normalizedTestJvm) { testJvmHome, testJvm ->
// Only change test JVM if it's not the one we are running the gradle build with
if (currentJavaHomePath.get() == testJvmHome) {
project.providers.provider<JavaLauncher?> { null }
} else {
// This is using internal APIs
val jvmSpec = org.gradle.jvm.toolchain.internal.SpecificInstallationToolchainSpec(
project.serviceOf<org.gradle.api.internal.provider.PropertyFactory>(),
project.file(testJvmHome)
)

// The provider always says that a value is present so we need to wrap it for proper error messages
project.javaToolchains.launcherFor(jvmSpec).orElse(project.providers.provider {
throw GradleException("Unable to find launcher for Java $testJvm. Does '$testJvmHome' point to a JDK?")
})
}
}.flatMap { it }.map { project.logger.info("testJvm launcher: ${it.executablePath}"); it }

private fun String.normalizeToJDKJavaHome(): Path {
val javaHome = project.file(this).toPath().toRealPath()
return if (javaHome.endsWith("jre")) javaHome.parent else javaHome
}

private val Project.javaToolchains: JavaToolchainService get() =
extensions.getByName("javaToolchains") as JavaToolchainService
}
2 changes: 1 addition & 1 deletion dd-java-agent/agent-bootstrap/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ jmh {
}

tasks.withType(Test).configureEach {
configureJvmArgs(
conditionalJvmArgs(
it,
JavaVersion.VERSION_16,
['--add-opens', 'java.base/java.net=ALL-UNNAMED'] // for HostNameResolverForkedTest
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
// Set properties before any plugins get loaded
ext {
minJavaVersionForTests = JavaVersion.VERSION_11
// By default tests with be compiled for `minJavaVersionForTests` version,
// but in this case we would like to avoid this since we would like to run with ZULU8
skipSettingTestJavaVersion = true
excludeJdk = ['SEMERU11', 'SEMERU17']
}

apply from: "$rootDir/gradle/java.gradle"
apply plugin: 'idea'

tracerJava {
addSourceSetFor(JavaVersion.VERSION_11) {
// By default tests with be compiled for `minJavaVersion` version,
// but in this case we would like to avoid this since we would like to run with ZULU8
applyForTestSources = false
}
}

testJvmConstraint {
minJavaVersion = JavaVersion.VERSION_11
excludeJdk = ['SEMERU11', 'SEMERU17']
}

minimumBranchCoverage = 0.5
minimumInstructionCoverage = 0.7

Expand Down
Loading
Loading