Skip to content
Draft
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
Original file line number Diff line number Diff line change
@@ -1,24 +1,53 @@
package datadog.buildlogic.smoketest

import org.gradle.api.Transformer
import org.gradle.api.file.RegularFile
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.Classpath
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
import java.util.jar.JarFile

/**
* A jar produced by the root build that needs to be forwarded into a [NestedGradleBuild].
*
* At execution time the task adds `-P${propertyName}=<absolute path of file>` to the nested
* Gradle invocation, so the inner build script can pick it up via `findProperty(...)`.
*
* Cached JVM and native fixtures can retain earlier embedded commit stamps.
*/
abstract class NestedBuildProjectJar {

@get:Input
abstract val propertyName: Property<String>

@get:InputFile
@get:PathSensitive(PathSensitivity.NONE)
/**
* Runtime classpath hashing tracks classes and resources: .classdata, manifests, instrumentation
* indexes, configuration files and native libraries. Changes to normalised contents invalidate
* application builds.
*/
@get:Classpath
abstract val file: RegularFileProperty

/**
* `project.version` is the base version (e.g. `1.69.0-SNAPSHOT`); `WriteVersionFile` appends `~<SHA>`.
* Runtime-classpath normalisation excludes the complete `*.version` resources.
* Tracking the base version separately invalidates application builds on version changes,
* while changing only the commit stamp allows reuse when the other inputs match.
*/
@get:Input
abstract val version: Property<String>

/** Gradle CI Visibility loads `dd-java-agent.version`, so its presence affects smoke results. */
@get:Input
val neededResourcePaths: Provider<List<String>>
get() = file.map(existingResourcePaths("dd-java-agent.version"))

private fun existingResourcePaths(vararg paths: String): Transformer<List<String>, RegularFile> =
Transformer { file ->
JarFile(file.asFile).use { jar ->
paths.filter { jar.getJarEntry(it)?.isDirectory == false }.sorted()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -163,17 +163,18 @@ abstract class NestedGradleBuild @Inject constructor(
@get:OutputDirectory
abstract val applicationBuildDir: DirectoryProperty

/** Forward a root-build jar as `-P<name>=<absolute path>` into the nested build. */
fun projectJar(name: String, file: Provider<RegularFile>) {
/** Forward a JAR with runtime-classpath normalisation and an explicit base [version]. */
fun projectJar(name: String, file: Provider<RegularFile>, version: Provider<String>) {
projectJars.add(
objects.newInstance<NestedBuildProjectJar>().apply {
propertyName.set(name)
this.file.set(file)
this.version.set(version)
},
)
}

/** Configure additional aspects of the nested build via a typed action. */
/** Configure a normalised JAR input, including its base version, via a typed action. */
fun projectJar(action: Action<NestedBuildProjectJar>) {
projectJars.add(
objects.newInstance<NestedBuildProjectJar>().also(action::execute),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -213,31 +213,31 @@ abstract class SmokeTestAppExtension @Inject constructor(
}

/**
* Forward the default `jar` artifact from [sourceProject] into the nested build as
* `-P<propertyName>=<absolute path>`. The jar is consumed via a resolvable [Configuration],
* which both establishes the correct task dependency and lets Gradle resolve the artifact
* lazily — no `evaluationDependsOn` is needed.
* Forward a JAR from [sourceProject] as `-P<propertyName>=<absolute path>`.
* [configuration] selects an outgoing artifact configuration, such as `shadow`;
* the default artifact is used when it is omitted. Resolution is lazy and carries
* the producing task dependency.
*
* Fingerprints use this project's runtime-classpath normalisation and track the
* producing project's version separately. Classes, `.classdata` and other tracked
* resources remain inputs; ZIP metadata and configured resource exclusions are ignored.
* Cached JVM applications and native executables can retain earlier embedded agent
* and API commit stamps when only excluded version metadata changes.
*/
fun projectJar(propertyName: String, sourceProject: Project) {
val cfg = createExtraJarConfiguration(propertyName)
project.dependencies.add(cfg.name, sourceProject)
addProjectJarFromConfiguration(propertyName, cfg)
}

/**
* Forward a non-default artifact configuration from [sourceProject]. Use this when the
* upstream project exposes its build output under a configuration other than the default
* (e.g. `shadowJar`).
*/
fun projectJar(propertyName: String, sourceProject: Project, configuration: String) {
@JvmOverloads
fun projectJar(propertyName: String, sourceProject: Project, configuration: String? = null) {
val cfg = createExtraJarConfiguration(propertyName)
project.dependencies.add(
cfg.name,
project.dependencies.project(
if (configuration == null) sourceProject else project.dependencies.project(
mapOf("path" to sourceProject.path, "configuration" to configuration),
),
)
addProjectJarFromConfiguration(propertyName, cfg)
projectJar(
propertyName,
project.layout.file(cfg.elements.map { files -> files.single().asFile }),
sourceProject.provider { sourceProject.version.toString() },
)
}

private fun createExtraJarConfiguration(propertyName: String): Configuration {
Expand All @@ -253,29 +253,15 @@ abstract class SmokeTestAppExtension @Inject constructor(

/**
* Lower-level overload for the rare case where the caller already has a provider of the
* file. The caller is responsible for the upstream task dependency.
* file and its base [version]. Uses the same normalisation as the project overload;
* the caller is responsible for the upstream task dependency.
*/
fun projectJar(propertyName: String, file: Provider<RegularFile>) {
fun projectJar(propertyName: String, file: Provider<RegularFile>, version: Provider<String>) {
projectJars.add(
project.objects.newInstance<NestedBuildProjectJar>().apply {
this.propertyName.set(propertyName)
this.file.set(file)
},
)
}

private fun addProjectJarFromConfiguration(propertyName: String, cfg: Configuration) {
projectJars.add(
project.objects.newInstance<NestedBuildProjectJar>().apply {
this.propertyName.set(propertyName)
// Configuration.elements yields a Provider that carries the producing task dependency,
// so wiring it into the task's @InputFile both tracks file contents and arranges build
// order.
this.file.set(
cfg.elements.map { files ->
project.objects.fileProperty().fileValue(files.single().asFile).get()
},
)
this.version.set(version)
},
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ import org.tabletest.junit.TableTest
import java.io.File
import java.nio.file.Files
import java.nio.file.Path
import java.util.jar.JarEntry
import java.util.jar.JarFile
import java.util.jar.JarOutputStream

/**
* End-to-end tests that drive the plugin through the Gradle Test Kit and a temporary,
Expand Down Expand Up @@ -66,6 +69,128 @@ class SmokeTestAppEndToEndTest {
assertThat(applicationOutput("libs/sample.jar")).exists()
}

@TableTest(
"scenario | configuration | change | expected ",
"commit stamp ignored | default | stamp | FROM_CACHE ",
"ZIP timestamps ignored | default | zip | FROM_CACHE ",
"ZIP entry order ignored | default | order | FROM_CACHE ",
"project version tracked | default | version | SUCCESS ",
"class contents tracked | default | class | SUCCESS ",
"classdata tracked | default | classdata | SUCCESS ",
"other resources tracked | default | resource | SUCCESS ",
"agent version added | default | added | SUCCESS ",
"agent version removed | default | removed | SUCCESS ",
"other version paths ignored | default | paths | FROM_CACHE ",
"shadow commit stamp ignored | shadow | stamp | FROM_CACHE ",
"shadow ZIP timestamps ignored | shadow | zip | FROM_CACHE ",
"shadow ZIP entry order ignored | shadow | order | FROM_CACHE ",
"shadow project version tracked | shadow | version | SUCCESS ",
"shadow class contents tracked | shadow | class | SUCCESS ",
"shadow classdata tracked | shadow | classdata | SUCCESS ",
"shadow other resources tracked | shadow | resource | SUCCESS ",
"shadow agent version added | shadow | added | SUCCESS ",
"shadow agent version removed | shadow | removed | SUCCESS ",
"shadow other versions ignored | shadow | paths | FROM_CACHE ",
)
fun `project JAR fingerprints ignore metadata and track source changes`(
configuration: String,
change: String,
expected: TaskOutcome,
) {
writeOuterSettings(withLocalBuildCache = true)
outerSettings.appendText("\ninclude(\":api\")\n")
val apiDirectory = projectDir.resolve("api").toFile().apply { mkdirs() }
File(apiDirectory, "build.gradle.kts").writeText(
"""
plugins { base }
version = providers.gradleProperty("apiVersion").getOrElse("1.0")
val prepareJar = tasks.register<Copy>("prepareJar") {
from("input.jar")
into(layout.buildDirectory.dir("libs"))
}
val outgoing = configurations.${if (configuration == "default") "named(\"default\")" else "create(\"shadow\")"} {
isCanBeConsumed = true
isCanBeResolved = false
}
artifacts.add(outgoing.name, layout.buildDirectory.file("libs/input.jar")) {
builtBy(prepareJar)
}
""".trimIndent(),
)
writeSmokeTestAppBuild(
"""
${smokeTestGradleApplication("packageApp", "embedded-api.jar", "api.path")}
projectJar("apiJar", project(":api")${if (configuration == "default") "" else ", \"shadow\""})
""",
extraImports = "import datadog.buildlogic.smoketest.NestedGradleBuild",
extraPreamble = """
normalization.runtimeClasspath.ignore("**/*.version")
tasks.withType<NestedGradleBuild>().configureEach {
actions.clear()
val inputJar = projectJars.flatMap { it.single().file }
val target = applicationBuildDir.file("embedded-api.jar")
doLast {
target.get().asFile.apply {
parentFile.mkdirs()
inputJar.get().asFile.copyTo(this, overwrite = true)
}
}
}
""",
)

fun writeApiJar(changed: Boolean) {
val entries = mutableMapOf(
"api.version" to if (changed && change == "stamp") "1.0~next" else "1.0~initial",
"example/Api.class" to if (changed && change == "class") "changed class" else "class",
"example/Agent.classdata" to if (changed && change == "classdata") "changed classdata" else "classdata",
"config.txt" to if (changed && change == "resource") "changed resource" else "resource",
)
if (changed && change == "paths") {
entries["optional.version"] = "1.0~initial"
}
if ((changed && change == "added") || (!changed && change == "removed")) {
entries["dd-java-agent.version"] = "1.0~initial"
}
JarOutputStream(File(apiDirectory, "input.jar").outputStream()).use { jar ->
val orderedEntries = if (changed && change == "order") entries.toList().reversed() else entries.toList()
orderedEntries.forEach { (name, contents) ->
jar.putNextEntry(JarEntry(name).apply {
time = if (changed && change == "zip") 1700000000000L else 1000000000000L
})
jar.write(contents.toByteArray(Charsets.UTF_8))
jar.closeEntry()
}
}
}

writeApiJar(changed = false)
// Re-snapshot the JAR rewritten outside Gradle between runs.
val first = runner("packageApp", "--build-cache", "--configuration-cache", "--no-watch-fs", "-PapiVersion=1.0").build()
assertThat(first.task(":api:prepareJar")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
assertThat(first.task(":packageApp")?.outcome).isEqualTo(TaskOutcome.SUCCESS)

applicationBuildDir.deleteRecursively()
writeApiJar(changed = true)
val second = runner(
"packageApp", "--build-cache", "--configuration-cache", "--no-watch-fs",
"-PapiVersion=${if (change == "version") "2.0" else "1.0"}",
).build()
if (change != "version") {
assertThat(second.task(":api:prepareJar")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
}
assertThat(second.task(":packageApp")?.outcome).isEqualTo(expected)
if (change != "version") {
assertThat(second.output).contains("Reusing configuration cache.")
}
JarFile(applicationOutput("embedded-api.jar")).use { jar ->
val stamp = jar.getInputStream(jar.getJarEntry("api.version")).bufferedReader().use { it.readText() }
assertThat(stamp).isEqualTo("1.0~initial")
assertThat(jar.getJarEntry("dd-java-agent.version") != null).isEqualTo(change == "added")
assertThat(jar.getJarEntry("optional.version")).isNull()
}
}

@Test
fun `nested Maven build produces the configured artifact`() {
writeOuterSettings()
Expand Down Expand Up @@ -373,7 +498,7 @@ class SmokeTestAppEndToEndTest {
fun `nested build receives native app environment and provider backed file inputs`() {
writeOuterSettings()
val graalvmHome = projectDir.resolve("test-graalvm").toFile()
File(projectDir.toFile(), "agent.jar").writeText("agent")
JarOutputStream(File(projectDir.toFile(), "agent.jar").outputStream()).use { }
writeSmokeTestAppBuild(
"""
${smokeTestGradleApplication(
Expand All @@ -385,7 +510,11 @@ class SmokeTestAppEndToEndTest {
environment.put("GRAALVM_HOME", providers.provider { layout.projectDirectory.dir("test-graalvm").asFile.absolutePath })
""",
)}
projectJar("agentPath", providers.provider { layout.projectDirectory.file("agent.jar") })
projectJar(
"agentPath",
providers.provider { layout.projectDirectory.file("agent.jar") },
providers.provider { "1.0" },
)
""",
)
writeInnerSettings()
Expand Down
2 changes: 2 additions & 0 deletions dd-java-agent/agent-jmxfetch/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ abstract class CopyMetricConfigsTask extends DefaultTask {
def configDirectoryPath = createConfigDirectory()
def configPaths = Files.find(this.inputDirectory.toPath(), 10, this::checkMetricsFile)
.map(p -> copyMetricsFile(configDirectoryPath, p))
// Keep the packaged list independent of filesystem traversal order.
.sorted()
.collect(Collectors.toList())
saveConfig(configDirectoryPath.parent, configPaths)
}
Expand Down
10 changes: 0 additions & 10 deletions dd-smoke-tests/apm-tracing-disabled/application/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,6 @@ java {
sourceCompatibility = '1.8'
}

if (hasProperty('apiJar')) {
dependencies {
implementation files(property('apiJar'))
}
} else {
dependencies {
implementation "com.datadoghq:dd-trace-api:+"
}
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
// OpenTracing 0.32.0 is the last release and is intentionally pinned: this smoke test
Expand Down
1 change: 0 additions & 1 deletion dd-smoke-tests/apm-tracing-disabled/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ smokeTestApp {
artifactPath = 'libs/apm-tracing-disabled-smoketest.jar'
sysProperty = 'datadog.smoketest.springboot.shadowJar.path'
}
projectJar('apiJar', project(':dd-trace-api'))
}

dependencies {
Expand Down
2 changes: 1 addition & 1 deletion dd-smoke-tests/apm-tracing-disabled/gradle.lockfile
Original file line number Diff line number Diff line change
Expand Up @@ -128,4 +128,4 @@ org.tabletest:tabletest-junit:1.2.2=testCompileClasspath,testRuntimeClasspath
org.tabletest:tabletest-parser:1.2.1=testCompileClasspath,testRuntimeClasspath
org.testcontainers:testcontainers:1.21.4=testCompileClasspath,testRuntimeClasspath
org.xmlresolver:xmlresolver:5.3.3=spotbugs
empty=annotationProcessor,runtimeClasspath,smokeTestAppExtraJarApiJar,spotbugsPlugins,testAnnotationProcessor,testFixturesAnnotationProcessor,testFixturesCompileClasspath,testFixturesRuntimeClasspath
empty=annotationProcessor,runtimeClasspath,spotbugsPlugins,testAnnotationProcessor,testFixturesAnnotationProcessor,testFixturesCompileClasspath,testFixturesRuntimeClasspath
2 changes: 1 addition & 1 deletion dd-smoke-tests/armeria-grpc/application/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ dependencies {
runtimeOnly 'ch.qos.logback:logback-classic:1.4.7'
runtimeOnly 'org.slf4j:log4j-over-slf4j:1.7.36'

if (hasProperty('apiJar')) {
if (project.hasProperty('apiJar')) {
implementation files(property('apiJar'))
} else {
implementation "com.datadoghq:dd-trace-api:+"
Expand Down
Loading
Loading