Add more unit tests for gradle resolver (#1457)
diff --git a/tests/com/github/bazelbuild/rules_jvm_external/resolver/gradle/BUILD.bazel b/tests/com/github/bazelbuild/rules_jvm_external/resolver/gradle/BUILD.bazel index 7a996ef..ee231e6 100644 --- a/tests/com/github/bazelbuild/rules_jvm_external/resolver/gradle/BUILD.bazel +++ b/tests/com/github/bazelbuild/rules_jvm_external/resolver/gradle/BUILD.bazel
@@ -5,10 +5,12 @@ name = "GradleResolverTest", timeout = "eternal", srcs = [ + "GradleModuleMetadataHelper.java", "GradleResolverTest.java", ], data = [ "//private/tools/java/com/github/bazelbuild/rules_jvm_external/resolver/gradle/data:gradle_build_templates", + "//tests/com/github/bazelbuild/rules_jvm_external/resolver/gradle/fixtures", ], env = { "RJE_VERBOSE": "true", @@ -27,6 +29,10 @@ "junit:junit", repository_name = "regression_testing_coursier", ), + artifact( + "com.google.guava:guava", + repository_name = "rules_jvm_external_deps", + ), "@bazel_tools//tools/java/runfiles", ], )
diff --git a/tests/com/github/bazelbuild/rules_jvm_external/resolver/gradle/GradleModuleMetadataHelper.java b/tests/com/github/bazelbuild/rules_jvm_external/resolver/gradle/GradleModuleMetadataHelper.java new file mode 100644 index 0000000..971d3dd --- /dev/null +++ b/tests/com/github/bazelbuild/rules_jvm_external/resolver/gradle/GradleModuleMetadataHelper.java
@@ -0,0 +1,127 @@ +// Copyright 2025 The Bazel Authors. All rights reserved. +// +// Licensed 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 +// +// http://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 com.github.bazelbuild.rules_jvm_external.resolver.gradle; + +import com.github.bazelbuild.rules_jvm_external.Coordinates; +import com.github.bazelbuild.rules_jvm_external.resolver.MavenRepo; +import java.io.IOException; +import java.io.Reader; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.Objects; +import javax.xml.stream.XMLEventFactory; +import javax.xml.stream.XMLEventReader; +import javax.xml.stream.XMLEventWriter; +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.events.XMLEvent; + +/** + * Helper class to manage gradle module metadata within ephemeral local maven repos for tests + * related to the gradle resolver + */ +public class GradleModuleMetadataHelper { + private final MavenRepo mavenRepo; + private final String GRADLE_POM_MARKER = "do_not_remove: published-with-gradle-metadata"; + + public GradleModuleMetadataHelper(MavenRepo mavenRepo) throws IOException { + Objects.requireNonNull(mavenRepo); + this.mavenRepo = mavenRepo; + } + + public void addToMavenRepo(Coordinates coordinates, String moduleMetadata) + throws IOException, XMLStreamException { + // Add this to the maven repo with a POM file + this.mavenRepo.add(coordinates); + + Path dir = this.mavenRepo.getPath().resolve(coordinates.toRepoPath()).getParent(); + + Path moduleFile = + dir.resolve(coordinates.getArtifactId() + "-" + coordinates.getVersion() + ".module"); + Files.write(moduleFile, moduleMetadata.getBytes()); + + Path pomFile = + dir.resolve(coordinates.getArtifactId() + "-" + coordinates.getVersion() + ".pom"); + + injectGradleMarkerInPom(pomFile); + } + + private void injectGradleMarkerInPom(Path pomPath) throws IOException, XMLStreamException { + // To resolve artifacts with gradle metadata, gradle looks for a marker comment in pom.xml + // e.g https://repo1.maven.org/maven2/com/squareup/okio/okio/3.6.0/okio-3.6.0.pom + // so we insert them for the same reason for the pom.xml in these test cases + + String content = Files.readString(pomPath, StandardCharsets.UTF_8); + if (content.contains(GRADLE_POM_MARKER)) { + return; // already has marker, nothing to do + } + + XMLInputFactory inFactory = XMLInputFactory.newInstance(); + XMLOutputFactory outFactory = XMLOutputFactory.newInstance(); + XMLEventFactory eventFactory = XMLEventFactory.newInstance(); + + // Use a temp buffer to hold rewritten XML + StringWriter buffer = new StringWriter(); + try (Reader in = Files.newBufferedReader(pomPath, StandardCharsets.UTF_8)) { + XMLEventReader r = inFactory.createXMLEventReader(in); + XMLEventWriter w = outFactory.createXMLEventWriter(buffer); + + boolean injected = false; + while (r.hasNext()) { + XMLEvent e = r.nextEvent(); + w.add(e); + + if (!injected + && e.isStartElement() + && e.asStartElement().getName().getLocalPart().equals("project")) { + w.add(eventFactory.createCharacters("\n")); + w.add( + eventFactory.createComment( + " This module was also published with a richer model, Gradle metadata, ")); + w.add(eventFactory.createCharacters("\n")); + w.add( + eventFactory.createComment( + " which should be used instead. Do not delete the following line which ")); + w.add(eventFactory.createCharacters("\n")); + w.add( + eventFactory.createComment( + " is to indicate to Gradle or any Gradle module metadata file consumer ")); + w.add(eventFactory.createCharacters("\n")); + w.add( + eventFactory.createComment( + " that they should prefer consuming it instead. ")); + w.add(eventFactory.createCharacters("\n")); + w.add( + eventFactory.createComment(" " + GRADLE_POM_MARKER + " ")); + w.add(eventFactory.createCharacters("\n")); + injected = true; + } + } + w.close(); + r.close(); + } + + Files.writeString( + pomPath, + buffer.toString(), + StandardCharsets.UTF_8, + StandardOpenOption.TRUNCATE_EXISTING, + StandardOpenOption.WRITE); + } +}
diff --git a/tests/com/github/bazelbuild/rules_jvm_external/resolver/gradle/GradleResolverTest.java b/tests/com/github/bazelbuild/rules_jvm_external/resolver/gradle/GradleResolverTest.java index ec741c7..783cb3e 100644 --- a/tests/com/github/bazelbuild/rules_jvm_external/resolver/gradle/GradleResolverTest.java +++ b/tests/com/github/bazelbuild/rules_jvm_external/resolver/gradle/GradleResolverTest.java
@@ -14,16 +14,119 @@ package com.github.bazelbuild.rules_jvm_external.resolver.gradle; +import static org.junit.Assert.assertEquals; + +import com.github.bazelbuild.rules_jvm_external.Coordinates; +import com.github.bazelbuild.rules_jvm_external.resolver.MavenRepo; import com.github.bazelbuild.rules_jvm_external.resolver.Resolver; import com.github.bazelbuild.rules_jvm_external.resolver.ResolverTestBase; import com.github.bazelbuild.rules_jvm_external.resolver.cmd.ResolverConfig; import com.github.bazelbuild.rules_jvm_external.resolver.events.EventListener; import com.github.bazelbuild.rules_jvm_external.resolver.netrc.Netrc; +import com.google.common.graph.Graph; +import com.google.devtools.build.runfiles.AutoBazelRepository; +import com.google.devtools.build.runfiles.Runfiles; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Set; +import javax.xml.stream.XMLStreamException; +import org.junit.Test; +@AutoBazelRepository public class GradleResolverTest extends ResolverTestBase { @Override protected Resolver getResolver(Netrc netrc, EventListener listener) { return new GradleResolver(netrc, ResolverConfig.DEFAULT_MAX_THREADS, listener); } + + @Test + public void resolvesSimpleJvmVariant() throws IOException, XMLStreamException { + // This test validates gradle can resolve a artifact using only gradle module metadata + // In this case, there's a root artifact com.example.sample which points to + // com.example.sample-jvm, which satisfies the runtimeClasspath configuration + // as it has the JVM variant attributes by default. + Coordinates baseCoordinates = new Coordinates("com.example:sample:1.0"); + Coordinates jvmCoordinates = new Coordinates("com.example:sample-jvm:1.0"); + MavenRepo mavenRepo = MavenRepo.create(); + GradleModuleMetadataHelper moduleMetadataHelper = new GradleModuleMetadataHelper(mavenRepo); + + Runfiles runfiles = + Runfiles.preload().withSourceRepository(AutoBazelRepository_GradleResolverTest.NAME); + Path baseMetadataPath = + Paths.get( + runfiles.rlocation( + "rules_jvm_external/tests/com/github/bazelbuild/rules_jvm_external/resolver/gradle/fixtures/simpleJvmVariant/sample-1.0.module")); + String baseMetadata = Files.readString(baseMetadataPath); + moduleMetadataHelper.addToMavenRepo(baseCoordinates, baseMetadata); + + Path jvmMetadataPath = + Paths.get( + runfiles.rlocation( + "rules_jvm_external/tests/com/github/bazelbuild/rules_jvm_external/resolver/gradle/fixtures/simpleJvmVariant/sample-jvm-1.0.module")); + String jvmMetadata = Files.readString(jvmMetadataPath); + moduleMetadataHelper.addToMavenRepo(jvmCoordinates, jvmMetadata); + + Graph<Coordinates> resolved = + resolver + .resolve(prepareRequestFor(mavenRepo.getPath().toUri(), baseCoordinates)) + .getResolution(); + + assertEquals(2, resolved.nodes().size()); + // sample-jvm resolves indirectly through sample using the gradle module metadata redirect + assertEquals(Set.of(baseCoordinates, jvmCoordinates), resolved.nodes()); + } + + @Test + public void resolvesJvmButNotAndroidVariant() throws IOException, XMLStreamException { + // This test validates a scenario similar to + // https://repo1.maven.org/maven2/com/squareup/okhttp3/okhttp/5.1.0/okhttp-5.1.0.module + // which supports 2 different coordinates with one base coordinate - one for JVM and android + // using variant selection. + // Right now, we only resolve the default runtime classpath configuration, so we'll only resolve + // the JVM variant + // and won't have the android variant + Coordinates baseCoordinates = new Coordinates("com.example:sample:1.0"); + Coordinates jvmCoordinates = new Coordinates("com.example:sample-jvm:1.0"); + Coordinates androidCoordinates = new Coordinates("com.example:sample-android:1.0"); + MavenRepo mavenRepo = MavenRepo.create(); + GradleModuleMetadataHelper moduleMetadataHelper = new GradleModuleMetadataHelper(mavenRepo); + + Runfiles runfiles = + Runfiles.preload().withSourceRepository(AutoBazelRepository_GradleResolverTest.NAME); + Path baseMetadataPath = + Paths.get( + runfiles.rlocation( + "rules_jvm_external/tests/com/github/bazelbuild/rules_jvm_external/resolver/gradle/fixtures/jvmAndAndroidVariants/sample-1.0.module")); + String baseMetadata = Files.readString(baseMetadataPath); + moduleMetadataHelper.addToMavenRepo(baseCoordinates, baseMetadata); + + Path jvmMetadataPath = + Paths.get( + runfiles.rlocation( + "rules_jvm_external/tests/com/github/bazelbuild/rules_jvm_external/resolver/gradle/fixtures/jvmAndAndroidVariants/sample-jvm-1.0.module")); + String jvmMetadata = Files.readString(jvmMetadataPath); + moduleMetadataHelper.addToMavenRepo(jvmCoordinates, jvmMetadata); + + Path androidMetadataPath = + Paths.get( + runfiles.rlocation( + "rules_jvm_external/tests/com/github/bazelbuild/rules_jvm_external/resolver/gradle/fixtures/jvmAndAndroidVariants/sample-android-1.0.module")); + String androidMetadata = Files.readString(androidMetadataPath); + moduleMetadataHelper.addToMavenRepo(androidCoordinates, androidMetadata); + + Graph<Coordinates> resolved = + resolver + .resolve(prepareRequestFor(mavenRepo.getPath().toUri(), baseCoordinates)) + .getResolution(); + + // sample-jvm resolves indirectly through sample using the gradle module metadata redirect + // but not sample-android as we don't resolve multiple variants currently. + assertEquals(2, resolved.nodes().size()); + // Once we support resolving android variant, this test should be updated to ensure + // sample-android is also resolved + assertEquals(Set.of(baseCoordinates, jvmCoordinates), resolved.nodes()); + } }
diff --git a/tests/com/github/bazelbuild/rules_jvm_external/resolver/gradle/fixtures/BUILD.bazel b/tests/com/github/bazelbuild/rules_jvm_external/resolver/gradle/fixtures/BUILD.bazel new file mode 100644 index 0000000..e6b201f --- /dev/null +++ b/tests/com/github/bazelbuild/rules_jvm_external/resolver/gradle/fixtures/BUILD.bazel
@@ -0,0 +1,11 @@ +filegroup( + name = "fixtures", + srcs = [ + "jvmAndAndroidVariants/sample-1.0.module", + "jvmAndAndroidVariants/sample-android-1.0.module", + "jvmAndAndroidVariants/sample-jvm-1.0.module", + "simpleJvmVariant/sample-1.0.module", + "simpleJvmVariant/sample-jvm-1.0.module", + ], + visibility = ["//tests/com/github/bazelbuild/rules_jvm_external:__subpackages__"], +)
diff --git a/tests/com/github/bazelbuild/rules_jvm_external/resolver/gradle/fixtures/jvmAndAndroidVariants/sample-1.0.module b/tests/com/github/bazelbuild/rules_jvm_external/resolver/gradle/fixtures/jvmAndAndroidVariants/sample-1.0.module new file mode 100644 index 0000000..459f61c --- /dev/null +++ b/tests/com/github/bazelbuild/rules_jvm_external/resolver/gradle/fixtures/jvmAndAndroidVariants/sample-1.0.module
@@ -0,0 +1,52 @@ +{ + "formatVersion": "1.1", + "component": { + "group": "com.example", + "module": "sample", + "version": "1.0", + "attributes": { "org.gradle.status": "release" } + }, + "variants": [ + { + "name": "commonMainMetadataElements", + "attributes": { + "org.gradle.category": "library", + "org.gradle.usage": "kotlin-metadata", + "org.jetbrains.kotlin.platform.type": "common" + }, + "files": [ + { "name": "sample-1.0-metadata.jar", "url": "sample-1.0-metadata.jar" } + ] + }, + { + "name": "jvmRuntimeElements", + "attributes": { + "org.gradle.category": "library", + "org.gradle.usage": "java-runtime", + "org.jetbrains.kotlin.platform.type": "jvm", + "org.gradle.libraryelements": "jar" + }, + "available-at": { + "group": "com.example", + "module": "sample-jvm", + "version": "1.0", + "url": "../../sample-jvm/1.0/sample-jvm-1.0.module" + } + }, + { + "name": "androidReleaseRuntimeElements", + "attributes": { + "org.gradle.category": "library", + "org.gradle.usage": "java-runtime", + "org.jetbrains.kotlin.platform.type": "androidJvm", + "org.gradle.libraryelements": "aar" + }, + "available-at": { + "group": "com.example", + "module": "sample-android", + "version": "1.0", + "url": "../../sample-android/1.0/sample-android-1.0.module" + } + } + ] +} \ No newline at end of file
diff --git a/tests/com/github/bazelbuild/rules_jvm_external/resolver/gradle/fixtures/jvmAndAndroidVariants/sample-android-1.0.module b/tests/com/github/bazelbuild/rules_jvm_external/resolver/gradle/fixtures/jvmAndAndroidVariants/sample-android-1.0.module new file mode 100644 index 0000000..2127966 --- /dev/null +++ b/tests/com/github/bazelbuild/rules_jvm_external/resolver/gradle/fixtures/jvmAndAndroidVariants/sample-android-1.0.module
@@ -0,0 +1,24 @@ +{ + "formatVersion": "1.1", + "component": { + "group": "com.example", + "module": "sample-android", + "version": "1.0", + "attributes": { "org.gradle.status": "release" } + }, + "variants": [ + { + "name": "releaseRuntimeElements", + "attributes": { + "org.gradle.category": "library", + "org.gradle.usage": "java-runtime", + "org.gradle.libraryelements": "aar", + "org.gradle.dependency.bundling": "external", + "org.jetbrains.kotlin.platform.type": "androidJvm" + }, + "files": [ + { "name": "sample-android-1.0.aar", "url": "sample-android-1.0.aar" } + ] + } + ] +} \ No newline at end of file
diff --git a/tests/com/github/bazelbuild/rules_jvm_external/resolver/gradle/fixtures/jvmAndAndroidVariants/sample-jvm-1.0.module b/tests/com/github/bazelbuild/rules_jvm_external/resolver/gradle/fixtures/jvmAndAndroidVariants/sample-jvm-1.0.module new file mode 100644 index 0000000..fe7a290 --- /dev/null +++ b/tests/com/github/bazelbuild/rules_jvm_external/resolver/gradle/fixtures/jvmAndAndroidVariants/sample-jvm-1.0.module
@@ -0,0 +1,37 @@ +{ + "formatVersion": "1.1", + "component": { + "group": "com.example", + "module": "sample-jvm", + "version": "1.0", + "attributes": { "org.gradle.status": "release" } + }, + "variants": [ + { + "name": "apiElements", + "attributes": { + "org.gradle.category": "library", + "org.gradle.usage": "java-api", + "org.gradle.libraryelements": "jar", + "org.jetbrains.kotlin.platform.type": "jvm" + }, + "files": [ + { "name": "sample-jvm-1.0.jar", "url": "sample-jvm-1.0.jar" } + ] + }, + { + "name": "runtimeElements", + "attributes": { + "org.gradle.category": "library", + "org.gradle.usage": "java-runtime", + "org.gradle.libraryelements": "jar", + "org.gradle.dependency.bundling": "external", + "org.gradle.jvm.version": 11, + "org.jetbrains.kotlin.platform.type": "jvm" + }, + "files": [ + { "name": "sample-jvm-1.0.jar", "url": "sample-jvm-1.0.jar" } + ] + } + ] +} \ No newline at end of file
diff --git a/tests/com/github/bazelbuild/rules_jvm_external/resolver/gradle/fixtures/simpleJvmVariant/sample-1.0.module b/tests/com/github/bazelbuild/rules_jvm_external/resolver/gradle/fixtures/simpleJvmVariant/sample-1.0.module new file mode 100644 index 0000000..387a96c --- /dev/null +++ b/tests/com/github/bazelbuild/rules_jvm_external/resolver/gradle/fixtures/simpleJvmVariant/sample-1.0.module
@@ -0,0 +1,26 @@ +{ + "formatVersion": "1.1", + "component": { + "group": "com.example", + "module": "sample", + "version": "1.0", + "attributes": { "org.gradle.status": "release" } + }, + "variants": [ + { + "name": "jvmRuntimeElements", + "attributes": { + "org.gradle.category": "library", + "org.gradle.libraryelements": "jar", + "org.gradle.usage": "java-runtime", + "org.jetbrains.kotlin.platform.type": "jvm" + }, + "available-at": { + "group": "com.example", + "module": "sample-jvm", + "version": "1.0", + "url": "../../sample-jvm/1.0/sample-jvm-1.0.module" + } + } + ] +} \ No newline at end of file
diff --git a/tests/com/github/bazelbuild/rules_jvm_external/resolver/gradle/fixtures/simpleJvmVariant/sample-jvm-1.0.module b/tests/com/github/bazelbuild/rules_jvm_external/resolver/gradle/fixtures/simpleJvmVariant/sample-jvm-1.0.module new file mode 100644 index 0000000..4dbb50d --- /dev/null +++ b/tests/com/github/bazelbuild/rules_jvm_external/resolver/gradle/fixtures/simpleJvmVariant/sample-jvm-1.0.module
@@ -0,0 +1,17 @@ +{ + "formatVersion": "1.1", + "component": { "group": "com.example", "module": "sample-jvm", "version": "1.0", + "attributes": { "org.gradle.status": "release" } }, + "variants": [ + { + "name": "runtimeElements", + "attributes": { + "org.gradle.category": "library", + "org.gradle.libraryelements": "jar", + "org.gradle.usage": "java-runtime", + "org.jetbrains.kotlin.platform.type": "jvm" + }, + "files": [{ "name": "sample-jvm-1.0.jar", "url": "sample-jvm-1.0.jar" }] + } + ] +}