Skip to content

test: add failing test for transitive runtime capability. #1456

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
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
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ antlr-shadowed = "4.13.2.0"
asm-relocated = "9.7.1.0"
caffeine = "3.1.8"
commons-io = "2.16.0"
dagp = "2.4.2"
dagp = "2.17.0"
error-prone = "2.26.1"
gradle-publish-plugin = "1.1.0"
grammar = "0.5"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.autonomousapps.jvm

import com.autonomousapps.jvm.projects.ImplRuntimeTestImplConfusionProject
import com.autonomousapps.jvm.projects.TransitiveRuntimeProject
import com.autonomousapps.utils.Colors
import spock.lang.PendingFeature

import static com.autonomousapps.utils.Runner.build
import static com.google.common.truth.Truth.assertThat
Expand Down Expand Up @@ -36,4 +38,20 @@ final class RuntimeOnlySpec extends AbstractJvmSpec {
where:
gradleVersion << gradleVersions()
}

@PendingFeature
def "transitive dependencies with runtime capabilities are added directly (#gradleVersion)"() {
Comment on lines +42 to +43
Copy link
Owner Author

@autonomousapps autonomousapps May 20, 2025

Choose a reason for hiding this comment

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

Getting this test to pass will be harder than originally expected. I've made a lot of assumptions about not providing "runtimeOnly" advice in the past.

given:
def project = new TransitiveRuntimeProject()
gradleProject = project.gradleProject

when:
build(gradleVersion, gradleProject.rootDir, 'buildHealth')

then:
assertThat(project.actualBuildHealth()).containsExactlyElementsIn(project.expectedBuildHealth)

where:
gradleVersion << gradleVersions()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,9 @@ final class SecurityProviderProject extends AbstractProject {
return actualProjectAdvice(gradleProject)
}

private final projAdvice = [
private final Set<Advice> projAdvice = [
Advice.ofChange(moduleCoordinates(conscryptUber), conscryptUber.configuration, 'runtimeOnly'),
] as Set<Advice>
]

final Set<ProjectAdvice> expectedBuildHealth = [
projectAdviceForDependencies(':proj', projAdvice)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.autonomousapps.jvm.projects

import com.autonomousapps.AbstractProject
import com.autonomousapps.kit.GradleProject
import com.autonomousapps.model.Advice
import com.autonomousapps.model.ProjectAdvice

import static com.autonomousapps.AdviceHelper.*
import static com.autonomousapps.kit.gradle.Dependency.implementation
import static com.autonomousapps.kit.gradle.dependencies.Dependencies.conscryptUber

final class TransitiveRuntimeProject extends AbstractProject {

private final conscryptUber = conscryptUber('api')
final GradleProject gradleProject

TransitiveRuntimeProject() {
this.gradleProject = build()
}

private GradleProject build() {
return newGradleProjectBuilder()
.withSubproject('consumer') { s ->
s.withBuildScript { bs ->
bs.plugins(javaLibrary)
bs.dependencies(implementation(':unused'))
}
}
.withSubproject('unused') { s ->
s.withBuildScript { bs ->
bs.plugins(javaLibrary)
bs.dependencies(conscryptUber)
}
}
.write()
}

Set<ProjectAdvice> actualBuildHealth() {
return actualProjectAdvice(gradleProject)
}

private final Set<Advice> consumerAdvice = [
Advice.ofAdd(moduleCoordinates(conscryptUber), 'runtimeOnly'),
Advice.ofRemove(projectCoordinates(':unused'), 'implementation'),
]

private final Set<Advice> unusedAdvice = [
Advice.ofChange(moduleCoordinates(conscryptUber), conscryptUber.configuration, 'runtimeOnly'),
]

final Set<ProjectAdvice> expectedBuildHealth = [
projectAdviceForDependencies(':consumer', consumerAdvice),
projectAdviceForDependencies(':unused', unusedAdvice),
]
}
2 changes: 1 addition & 1 deletion src/main/kotlin/com/autonomousapps/model/Advice.kt
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ data class Advice(
}.isTrue()

/** If this is advice to remove or downgrade a dependency. */
fun isDowngrade(): Boolean = (isRemove() || isCompileOnly() || isRuntimeOnly())
fun isDowngrade(): Boolean = isRemove() || (!isAnyAdd() && (isCompileOnly() || isRuntimeOnly()))
Copy link
Owner Author

Choose a reason for hiding this comment

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

This was a bug.


/** If this is advice to add a dependency, or change an existing dependency to make it api-like. */
fun isUpgrade(): Boolean = isAnyAdd() || (isAnyChange() && isToApiLike())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
package com.autonomousapps.model.declaration.internal

import com.autonomousapps.internal.utils.reallyAll
import com.autonomousapps.model.declaration.internal.Bucket.Companion.VISIBLE_TO_TEST_SOURCE
import com.autonomousapps.model.declaration.internal.Bucket.Companion.VISIBLE_TO_TEST_COMPILE
import com.autonomousapps.model.declaration.internal.Bucket.Companion.VISIBLE_TO_TEST_RUNTIME
import com.autonomousapps.model.internal.intermediates.Usage
import com.squareup.moshi.JsonClass

Expand Down Expand Up @@ -45,29 +46,64 @@ internal enum class Bucket(val value: String) {

/**
* [Declarations][Declaration] in these buckets are visible from
* [SourceKind.MAIN][com.autonomousapps.model.source.SourceKind.MAIN] to
* [SourceKind.TEST][com.autonomousapps.model.source.SourceKind.TEST] and
* [SourceKind.ANDROID_TEST][com.autonomousapps.model.source.SourceKind.ANDROID_TEST]. This is necessary
* for correct advice relating to test source.
* [SourceKind.MAIN][com.autonomousapps.model.source.SourceKind.MAIN_KIND] to
* [SourceKind.TEST][com.autonomousapps.model.source.SourceKind.TEST_KIND] and
* [SourceKind.ANDROID_TEST][com.autonomousapps.model.source.SourceKind.ANDROID_TEST_KIND] compile classpaths. This
* is necessary for correct advice relating to test source.
*
* TODO(tsr): wait, is this actually true for ANNOTATION_PROCESSOR as well? That seems like an error. Oh, maybe it
* was true for an older version of Gradle?
* @see [VISIBLE_TO_TEST_RUNTIME]
*/
private val VISIBLE_TO_TEST_SOURCE = listOf(API, IMPL, ANNOTATION_PROCESSOR)
private val VISIBLE_TO_TEST_COMPILE = listOf(API, IMPL)

/**
* A dependency is visible from main to test source iff it is in the correct bucket ([VISIBLE_TO_TEST_SOURCE]) _and_
* if it is declared on either the [API] or [IMPL] configurations.
* [Declarations][Declaration] in these buckets are visible from
* [SourceKind.MAIN][com.autonomousapps.model.source.SourceKind.MAIN_KIND] to
* [SourceKind.TEST][com.autonomousapps.model.source.SourceKind.TEST_KIND] and
* [SourceKind.ANDROID_TEST][com.autonomousapps.model.source.SourceKind.ANDROID_TEST_KIND] runtime classpaths. This
* is necessary for correct advice relating to test source.
*
* @see [VISIBLE_TO_TEST_COMPILE]
*/
private val VISIBLE_TO_TEST_RUNTIME = listOf(API, IMPL, RUNTIME_ONLY)

fun determineVisibility(usages: Set<Usage>, declarations: Set<Declaration>): Visibility {
val compileVisibility = isVisibleToTestCompileClasspath(usages, declarations)
val runtimeVisibility = isVisibleToTestRuntimeClasspath(usages, declarations)

return Visibility(forCompile = compileVisibility, forRuntime = runtimeVisibility)
}

/**
* A dependency is visible from main to test source iff it is in the correct bucket ([VISIBLE_TO_TEST_COMPILE])
* _and_ if it is declared on any of the [VISIBLE_TO_TEST_COMPILE] configurations.
*
* Note that the `compileOnly` configuration _is not_ visible to the `testImplementation` configuration.
*
* @see <a href="https://docs.gradle.org/current/userguide/java_plugin.html#resolvable_configurations.">Java configurations</a>
*/
fun isVisibleToTestSource(usages: Set<Usage>, declarations: Set<Declaration>): Boolean {
fun isVisibleToTestCompileClasspath(usages: Set<Usage>, declarations: Set<Declaration>): Boolean {
return isVisibleIn(VISIBLE_TO_TEST_COMPILE, usages, declarations)
}

/**
* A dependency is visible from main to test source iff it is in the correct bucket ([VISIBLE_TO_TEST_RUNTIME])
* _and_ if it is declared on any of the [VISIBLE_TO_TEST_RUNTIME] configurations.
*
* Note that the `compileOnly` configuration _is not_ visible to the `testImplementation` configuration.
*
* @see <a href="https://docs.gradle.org/current/userguide/java_plugin.html#resolvable_configurations.">Java configurations</a>
*/
fun isVisibleToTestRuntimeClasspath(usages: Set<Usage>, declarations: Set<Declaration>): Boolean {
return isVisibleIn(VISIBLE_TO_TEST_RUNTIME, usages, declarations)
}

private fun isVisibleIn(buckets: List<Bucket>, usages: Set<Usage>, declarations: Set<Declaration>): Boolean {
return usages.reallyAll { usage ->
VISIBLE_TO_TEST_SOURCE.any { it == usage.bucket }
&& declarations.any { API.matches(it) || IMPL.matches(it) }
buckets.any { it == usage.bucket }
&& declarations.any { declaration -> buckets.any { it.matches(declaration) } }
}
}
}

class Visibility(val forCompile: Boolean, val forRuntime: Boolean)
}
72 changes: 4 additions & 68 deletions src/main/kotlin/com/autonomousapps/tasks/ComputeAdviceTask.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
package com.autonomousapps.tasks

import com.autonomousapps.extension.DependenciesHandler
import com.autonomousapps.graph.Graphs.children
import com.autonomousapps.graph.Graphs.root
import com.autonomousapps.internal.Bundles
import com.autonomousapps.internal.utils.*
import com.autonomousapps.internal.utils.CoordinatesString.Companion.toStringCoordinates
Expand All @@ -14,9 +12,7 @@ import com.autonomousapps.model.declaration.internal.Configurations
import com.autonomousapps.model.declaration.internal.Declaration
import com.autonomousapps.model.internal.DependencyGraphView
import com.autonomousapps.model.internal.intermediates.*
import com.autonomousapps.model.source.SourceKind
import com.autonomousapps.transform.StandardTransform
import com.google.common.collect.SetMultimap
import org.gradle.api.DefaultTask
import org.gradle.api.file.RegularFile
import org.gradle.api.file.RegularFileProperty
Expand Down Expand Up @@ -187,8 +183,6 @@ abstract class ComputeAdviceTask @Inject constructor(
val explicitSourceSets = parameters.explicitSourceSets.get()
val isAndroidProject = parameters.android.get()
val isKaptApplied = parameters.kapt.get()
val directDependencies = computeDirectDependenciesMap(dependencyGraph)
val dependenciesToClasspaths = computeDependenciesToClasspathsMap(dependencyGraph)

val ignoreKtx = parameters.ignoreKtx.get()

Expand All @@ -207,8 +201,7 @@ abstract class ComputeAdviceTask @Inject constructor(
dependencyUsages = dependencyUsages,
annotationProcessorUsages = annotationProcessorUsages,
declarations = declarations,
directDependencies = directDependencies,
dependenciesToClasspaths = dependenciesToClasspaths,
dependencyGraph = dependencyGraph,
supportedSourceSets = supportedSourceSets,
explicitSourceSets = explicitSourceSets,
isAndroidProject = isAndroidProject,
Expand Down Expand Up @@ -244,60 +237,6 @@ abstract class ComputeAdviceTask @Inject constructor(

return Warning(duplicateClassesReports)
}

/**
* Returns the set of direct (non-transitive) dependencies from [dependencyGraph], associated with the source sets
* ([Variant.variant][com.autonomousapps.model.source.SourceKind]) they're related to.
*
* These are _direct_ dependencies that are not _declared_ because they're coming from associated classpaths. For
* example, the `test` source set extends from the `main` source set (and also the compile and runtime classpaths).
*/
private fun computeDirectDependenciesMap(
dependencyGraph: Map<String, DependencyGraphView>,
): SetMultimap<String, SourceKind> {
return newSetMultimap<String, SourceKind>().apply {
dependencyGraph.values.map { graphView ->
val root = graphView.graph.root()
graphView.graph.children(root).forEach { directDependency ->
// An attempt to normalize the identifier
val identifier = if (directDependency is IncludedBuildCoordinates) {
directDependency.resolvedProject.identifier
} else {
directDependency.identifier
}

put(identifier, graphView.sourceKind)
}
}
}
}

/**
* This results in a map like:
* * "group:name:1.0" -> (compileClasspath, runtimeClasspath)
* * ":project" -> (compileClasspath)
*
* etc.
*/
private fun computeDependenciesToClasspathsMap(
dependencyGraph: Map<String, DependencyGraphView>,
): SetMultimap<String, String> {
return newSetMultimap<String, String>().apply {
dependencyGraph.values.map { graphView ->
graphView.graph.nodes().forEach { node ->
// coordinate with `StandardTransform`
// An attempt to normalize the identifier
val identifier = if (node is IncludedBuildCoordinates) {
node.resolvedProject.identifier
} else {
node.identifier
}

put(identifier, graphView.configurationName)
}
}
}
}
}
}

Expand Down Expand Up @@ -335,8 +274,7 @@ internal class DependencyAdviceBuilder(
private val dependencyUsages: Map<Coordinates, Set<Usage>>,
private val annotationProcessorUsages: Map<Coordinates, Set<Usage>>,
private val declarations: Set<Declaration>,
private val directDependencies: SetMultimap<String, SourceKind>,
private val dependenciesToClasspaths: SetMultimap<String, String>,
private val dependencyGraph: Map<String, DependencyGraphView>,
private val supportedSourceSets: Set<String>,
private val explicitSourceSets: Set<String>,
private val isAndroidProject: Boolean,
Expand Down Expand Up @@ -376,8 +314,7 @@ internal class DependencyAdviceBuilder(
StandardTransform(
coordinates = coordinates,
declarations = declarations,
directDependencies = directDependencies,
dependenciesToClasspaths = dependenciesToClasspaths,
dependencyGraph = dependencyGraph,
supportedSourceSets = supportedSourceSets,
buildPath = buildPath,
explicitSourceSets = explicitSourceSets,
Expand Down Expand Up @@ -438,8 +375,7 @@ internal class DependencyAdviceBuilder(
StandardTransform(
coordinates = coordinates,
declarations = declarations,
directDependencies = emptySetMultimap(),
dependenciesToClasspaths = emptySetMultimap(),
dependencyGraph = emptyMap(),
supportedSourceSets = supportedSourceSets,
buildPath = buildPath,
explicitSourceSets = explicitSourceSets,
Expand Down
Loading