Skip to content
Merged
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
29 changes: 27 additions & 2 deletions buildSrc/src/test/kotlin/datadog/gradle/plugin/GradleFixture.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,13 @@ open class GradleFixture {

private val testKitDir: File get() = sharedTestKitDir

private val repositoryProxyInitScript: File
get() = File(testKitDir, REPOSITORY_PROXY_INIT_SCRIPT)

companion object {
/** Init script copied into the testkit dir and applied to every TestKit build. */
private const val REPOSITORY_PROXY_INIT_SCRIPT = "repository-proxy.init.gradle.kts"

// JVM-wide testkit dir shared across all GradleFixture instances. One daemon
// pool serves every test method, so kotlinc work on .gradle.kts scripts is
// amortized instead of re-paid per test (recovers the +77 % wall-time
Expand All @@ -35,10 +41,17 @@ open class GradleFixture {
// fields.
private val sharedTestKitDir: File by lazy {
Files.createTempDirectory("gradle-testkit-").toFile().also { dir ->
// Register the cleanup hook before anything that can throw, so a failure below
// does not leak the temp dir.
Runtime.getRuntime().addShutdownHook(Thread {
stopDaemonsIn(dir)
dir.deleteRecursively()
})
val initScript = File(dir, REPOSITORY_PROXY_INIT_SCRIPT)
GradleFixture::class.java
.getResourceAsStream("/$REPOSITORY_PROXY_INIT_SCRIPT")
?.use { input -> initScript.outputStream().use(input::copyTo) }
?: error("Missing $REPOSITORY_PROXY_INIT_SCRIPT test resource")
}
}

Expand Down Expand Up @@ -110,6 +123,9 @@ open class GradleFixture {
* @param args Gradle task names and arguments
* @param expectFailure Whether the build is expected to fail
* @param env Environment variables to set (merged with system environment)
* @param unsetEnv Environment variables to remove from the system environment. Needed to
* exercise "no repository proxy configured" behaviour, since CI exports
* MAVEN_REPOSITORY_PROXY/GRADLE_PLUGIN_PROXY globally and [env] can only add.
* @param forwardOutput Forward the build's stdout/stderr to the test's output
* @param gradleProjectDir Override the project directory used by Gradle (useful for git worktree tests);
* defaults to the fixture's project directory.
Expand All @@ -119,6 +135,7 @@ open class GradleFixture {
vararg args: String,
expectFailure: Boolean = false,
env: Map<String, String> = emptyMap(),
unsetEnv: Set<String> = emptySet(),
forwardOutput: Boolean = false,
gradleProjectDir: File = projectDir,
): BuildResult {
Expand All @@ -127,8 +144,8 @@ open class GradleFixture {
.withPluginClasspath()
.withProjectDir(gradleProjectDir)
// Using withDebug prevents starting a daemon, but it doesn't work with withEnvironment
.withEnvironment(System.getenv() + env)
.withArguments(*args)
.withEnvironment(System.getenv() - unsetEnv + env)
.withArguments("--init-script", repositoryProxyInitScript.absolutePath, *args)
if (forwardOutput) {
runner.forwardOutput()
}
Expand Down Expand Up @@ -225,6 +242,14 @@ open class GradleFixture {
return builder.parse(xmlFile)
}

/**
* Creates a fake Maven repository under the project directory.
*
* Pass its [MavenRepoFixture.repoUrl] as `MAVEN_REPOSITORY_PROXY` to [run] to make it
* shadow Maven Central for the TestKit build.
*/
fun createMavenRepoFixture(): MavenRepoFixture = MavenRepoFixture(projectDir)

/**
* Returns a File handle under the project directory.
* Does not touch the filesystem.
Expand Down
147 changes: 147 additions & 0 deletions buildSrc/src/test/kotlin/datadog/gradle/plugin/GradleFixtureTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package datadog.gradle.plugin

import org.assertj.core.api.Assertions.assertThat
import org.gradle.testkit.runner.TaskOutcome.SUCCESS
import org.junit.jupiter.api.Test

class GradleFixtureTest : GradleFixture() {

private companion object {
const val MAVEN_CENTRAL = "https://repo.maven.apache.org/maven2"
const val PLUGIN_PORTAL = "https://plugins.gradle.org/m2"
}

@Test
fun `TestKit build routes Maven Central through configured proxy`() {
val proxyRepository = createMavenRepoFixture()
proxyRepository.publishVersions("com.example", "proxy-only", listOf("1.0.0"))
writeRootProject(
"""
plugins {
id("java")
}

repositories {
mavenCentral()
}

dependencies {
implementation("com.example:proxy-only:1.0.0")
}
"""
)
writeJavaSource("Example", "public class Example {}")

val result = run(
"compileJava",
env = mapOf("MAVEN_REPOSITORY_PROXY" to proxyRepository.repoUrl),
)

assertThat(result.task(":compileJava")?.outcome).isEqualTo(SUCCESS)
}

/**
* Contributors without access to the internal Depot mirror run with no proxy configured.
* The init script must then be completely inert. Assert on the repository URLs of every
* container it touches rather than resolving anything, so the check stays hermetic.
*/
@Test
fun `TestKit build leaves public repositories untouched when no proxy is configured`() {
writeSettings(
"""
import org.gradle.api.artifacts.repositories.MavenArtifactRepository

pluginManagement {
repositories {
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositories {
mavenCentral()
}
}
rootProject.name = "no-proxy"

gradle.settingsEvaluated {
(pluginManagement.repositories + dependencyResolutionManagement.repositories)
.filterIsInstance<MavenArtifactRepository>()
.forEach { println("REPOSITORY=" + it.url) }
}
"""
)
writeRootProject(
"""
import org.gradle.api.artifacts.repositories.MavenArtifactRepository

buildscript {
repositories {
mavenCentral()
}
}

plugins {
id("java")
}

tasks.register("printRepositories") {
val urls = buildscript.repositories
.filterIsInstance<MavenArtifactRepository>()
.map { it.url.toString() }
doLast { urls.forEach { println("REPOSITORY=" + it) } }
}
"""
)

val result = run(
"printRepositories",
unsetEnv = setOf("MAVEN_REPOSITORY_PROXY", "GRADLE_PLUGIN_PROXY"),
)

val repositories = result.output.lines()
.filter { it.startsWith("REPOSITORY=") }
.map { it.removePrefix("REPOSITORY=") }
assertThat(repositories)
.withFailMessage("Expected repositories to be printed, output was:\n%s", result.output)
.isNotEmpty()
assertThat(repositories)
.withFailMessage("No proxy is configured, so nothing may be rewritten, got %s", repositories)
.allMatch { it.startsWith(MAVEN_CENTRAL) || it.startsWith(PLUGIN_PORTAL) }
assertThat(repositories).anyMatch { it.startsWith(MAVEN_CENTRAL) }
assertThat(repositories).anyMatch { it.startsWith(PLUGIN_PORTAL) }
}

@Test
fun `TestKit build routes settings-level Maven Central through configured proxy`() {
val proxyRepository = createMavenRepoFixture()
proxyRepository.publishVersions("com.example", "settings-proxy-only", listOf("1.0.0"))
writeSettings(
"""
dependencyResolutionManagement {
repositories {
mavenCentral()
}
}
"""
)
writeRootProject(
"""
plugins {
id("java")
}

dependencies {
implementation("com.example:settings-proxy-only:1.0.0")
}
"""
)
writeJavaSource("Example", "public class Example {}")

val result = run(
"compileJava",
env = mapOf("MAVEN_REPOSITORY_PROXY" to proxyRepository.repoUrl),
)

assertThat(result.task(":compileJava")?.outcome).isEqualTo(SUCCESS)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import org.eclipse.aether.resolution.VersionRangeResult
import org.eclipse.aether.util.version.GenericVersionScheme
import org.gradle.api.GradleException
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable
import org.junit.jupiter.api.io.TempDir
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.CsvSource
Expand All @@ -19,6 +21,8 @@ import java.util.concurrent.atomic.AtomicInteger
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy

private const val MAVEN_CENTRAL_URL = "https://repo1.maven.org/maven2/"

class MuzzleMavenRepoUtilsTest {

@TempDir
Expand Down Expand Up @@ -109,6 +113,31 @@ class MuzzleMavenRepoUtilsTest {
.hasMessageContaining("Backoff:\n disabled")
}

// The two tests below are mutually exclusive: MAVEN_REPOSITORY_PROXY is read from the real
// environment (defaultMuzzleRepos deliberately does not take it as a parameter), so each of
// them covers the branch its environment can reach -- unset locally, set in CI.

@Test
@DisabledIfEnvironmentVariable(
named = "MAVEN_REPOSITORY_PROXY",
matches = ".*",
disabledReason = "A mirror is configured; the proxy variant of this test covers that case"
)
fun `defaultMuzzleRepos is Maven Central alone when no proxy is configured`() {
assertThat(MuzzleMavenRepoUtils.defaultMuzzleRepos().map { it.id to it.url })
.containsExactly("central" to MAVEN_CENTRAL_URL)
}

@Test
@EnabledIfEnvironmentVariable(named = "MAVEN_REPOSITORY_PROXY", matches = ".*")
fun `defaultMuzzleRepos queries the configured proxy before Maven Central`() {
val proxyUrl = System.getenv("MAVEN_REPOSITORY_PROXY")

// Central stays in the list as a fallback, but the proxy is consulted first.
assertThat(MuzzleMavenRepoUtils.defaultMuzzleRepos().map { it.id to it.url })
.containsExactly("central-proxy" to proxyUrl, "central" to MAVEN_CENTRAL_URL)
}

@Test
fun `resolveVersionRange failure includes thrown resolution failure details`() {
val directive = MuzzleDirective().apply {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,14 @@ class MuzzlePluginFunctionalTest : MuzzlePluginTestFixture() {
module = "with-transitive",
versions = listOf("1.0.0")
)
// Publish the transitive dependency too, so the test still discriminates: without the
// exclusion guava resolves and lands on the classpath, and the scan plugin below fails.
// (Resolving it from the real Maven Central would defeat routing everything through the proxy.)
mavenRepoFixture.publishVersions(
group = "com.google.guava",
module = "guava",
versions = listOf("31.0-jre")
)

// Manually create a POM with a transitive dependency
// Write into MavenRepoFixture's repoDir, not GradleFixture's projectDir.
Expand Down Expand Up @@ -575,7 +583,6 @@ class MuzzlePluginFunctionalTest : MuzzlePluginTestFixture() {
artifact()
}
}
mavenCentral()
}

muzzle {
Expand All @@ -589,15 +596,15 @@ class MuzzlePluginFunctionalTest : MuzzlePluginTestFixture() {
"""
)

// Scan plugin verifies that guava is NOT in the classpath (it was excluded)
// Scan plugin verifies that guava is NOT in the classpath (it was excluded).
// The fixture jar carries no guava classes, so probe the Maven descriptor every
// MavenRepoFixture artifact embeds instead of loading a class.
writeScanPlugin(
"""
try {
testApplicationClassLoader.loadClass("com.google.common.collect.ImmutableList");
if (testApplicationClassLoader.getResource("META-INF/maven/com.google.guava/guava/pom.properties") != null) {
throw new RuntimeException("Unexpected excluded dependency (guava) SHOULD NOT be in test classpath but was found");
} catch (ClassNotFoundException e) {
System.out.println("Excluded dependency (guava) correctly not in test classpath");
}
System.out.println("Excluded dependency (guava) correctly not in test classpath");
"""
)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package datadog.gradle.plugin.muzzle

import datadog.gradle.plugin.GradleFixture
import datadog.gradle.plugin.MavenRepoFixture
import org.intellij.lang.annotations.Language
import java.io.File

Expand All @@ -10,8 +9,6 @@ import java.io.File
* Extends GradleFixture with muzzle-specific functionality.
*/
open class MuzzlePluginTestFixture : GradleFixture() {
fun createMavenRepoFixture(): MavenRepoFixture = MavenRepoFixture(projectDir)

/**
* Writes the basic Gradle project structure for muzzle testing.
* Creates a multi-project build with agent-bootstrap, agent-tooling, and instrumentation modules.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,41 @@
package datadog.gradle.plugin.muzzle

import datadog.gradle.plugin.MavenRepoFixture
import org.eclipse.aether.artifact.Artifact
import org.eclipse.aether.artifact.DefaultArtifact
import org.eclipse.aether.repository.RemoteRepository
import org.eclipse.aether.resolution.VersionRangeRequest
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir
import java.io.File

class RangeQueryTest {
@TempDir
lateinit var tempDir: File

private val system = MuzzleMavenRepoUtils.newRepositorySystem()
private val session = MuzzleMavenRepoUtils.newRepositorySystemSession(system)

@Test
fun `test range request`() {
// compile group: 'org.codehaus.groovy', name: 'groovy-all', version: '2.5.0', ext: 'pom'
val directiveArtifact: Artifact = DefaultArtifact("org.codehaus.groovy", "groovy-all", "jar", "[2.5.0,2.5.8)")
val repository = MavenRepoFixture(tempDir)
repository.publishVersions(
"org.codehaus.groovy",
"groovy-all",
(0..8).map { "2.5.$it" },
)
val directiveArtifact: Artifact =
DefaultArtifact("org.codehaus.groovy", "groovy-all", "jar", "[2.5.0,2.5.8)")
val rangeRequest = VersionRangeRequest().apply {
repositories = MuzzleMavenRepoUtils.defaultMuzzleRepos()
repositories =
listOf(RemoteRepository.Builder("fixture", "default", repository.repoUrl).build())
artifact = directiveArtifact
}

// This call makes an actual network request, which may fail if network access is limited.
val rangeResult = system.resolveVersionRange(session, rangeRequest)

assertThat(rangeResult.versions.size).isGreaterThanOrEqualTo(8)
assertThat(rangeResult.versions.map { it.toString() })
.containsExactlyElementsOf((0..7).map { "2.5.$it" })
}
}
Loading