diff --git a/src/functionalTest/groovy/com/autonomousapps/android/ResSpec.groovy b/src/functionalTest/groovy/com/autonomousapps/android/ResSpec.groovy
index 6831b80d8..89e17d20e 100644
--- a/src/functionalTest/groovy/com/autonomousapps/android/ResSpec.groovy
+++ b/src/functionalTest/groovy/com/autonomousapps/android/ResSpec.groovy
@@ -4,9 +4,11 @@ package com.autonomousapps.android
import com.autonomousapps.android.projects.*
import org.gradle.testkit.runner.TaskOutcome
+import spock.lang.PendingFeature
import static com.autonomousapps.advice.truth.BuildHealthSubject.buildHealth
-import static com.autonomousapps.utils.Runner.build
+import static com.autonomousapps.kit.GradleBuilder.build
+import static com.autonomousapps.kit.GradleBuilder.buildAndFail
import static com.google.common.truth.Truth.assertAbout
@SuppressWarnings("GroovyAssignabilityCheck")
@@ -203,4 +205,35 @@ final class ResSpec extends AbstractAndroidSpec {
where:
[gradleVersion, agpVersion] << gradleAgpMatrix()
}
+
+ // TODO(tsr): this test case demonstrates (or more accurately: fails to demonstrate) any issue when multiple Android
+ // libraries provide a resource with the same name, and the consumer side only references that resource in an XML
+ // file (so there's no qualified reference in code for more accurate detection).
+ // See e.g. https://developer.android.com/studio/projects/android-library#Considerations for information on resource
+ // merging; and
+ // https://developer.android.com/reference/tools/gradle-api/8.3/null/com/android/build/api/dsl/CommonExtension#setResourcePrefix(kotlin.String)
+ // for an AGP DSL that can help mitigate the problem (set unique prefixes everywhere and use them).
+ // This is annotated with `@PendingFeature` because the assertion is that the build fails (but the build succeeds). If
+ // there is ever any improvement in this scenario, this case will begin to really fail and then we can update the
+ // assertions.
+ @PendingFeature(reason = "https://github.com/autonomousapps/dependency-analysis-gradle-plugin/issues/1731")
+ def "handles duplicate res-by-res producers (#gradleVersion AGP #agpVersion)"() {
+ given:
+ def project = new ResByResProject(agpVersion)
+ gradleProject = project.gradleProject
+
+ when:
+ buildAndFail(gradleVersion, gradleProject.rootDir, 'buildHealth')
+ // The output is identical
+// build(gradleVersion, gradleProject.rootDir, ':app:reason', '--id', ':res1')
+// build(gradleVersion, gradleProject.rootDir, ':app:reason', '--id', ':res2')
+
+ then:
+ assertAbout(buildHealth())
+ .that(project.actualBuildHealth())
+ .isEquivalentIgnoringModuleAdviceAndWarnings(project.expectedBuildHealth)
+
+ where:
+ [gradleVersion, agpVersion] << gradleAgpMatrix()
+ }
}
diff --git a/src/functionalTest/groovy/com/autonomousapps/android/projects/AbstractAndroidProject.groovy b/src/functionalTest/groovy/com/autonomousapps/android/projects/AbstractAndroidProject.groovy
index 50e3b7421..12f0e5b07 100644
--- a/src/functionalTest/groovy/com/autonomousapps/android/projects/AbstractAndroidProject.groovy
+++ b/src/functionalTest/groovy/com/autonomousapps/android/projects/AbstractAndroidProject.groovy
@@ -110,6 +110,10 @@ abstract class AbstractAndroidProject extends AbstractProject {
return AndroidManifest.appWithoutPackage(namespace)
}
+ protected AndroidManifest appEmpty() {
+ return AndroidManifest.appEmpty()
+ }
+
protected AndroidManifest libraryManifest(String namespace = DEFAULT_LIB_NAMESPACE) {
return null
}
diff --git a/src/functionalTest/groovy/com/autonomousapps/android/projects/ResByResProject.groovy b/src/functionalTest/groovy/com/autonomousapps/android/projects/ResByResProject.groovy
new file mode 100644
index 000000000..5c6d43d1b
--- /dev/null
+++ b/src/functionalTest/groovy/com/autonomousapps/android/projects/ResByResProject.groovy
@@ -0,0 +1,92 @@
+// Copyright (c) 2026. Tony Robalik.
+// SPDX-License-Identifier: Apache-2.0
+package com.autonomousapps.android.projects
+
+import com.autonomousapps.kit.GradleProject
+import com.autonomousapps.kit.android.AndroidLayout
+import com.autonomousapps.kit.android.AndroidStringRes
+import com.autonomousapps.kit.android.AndroidSubproject
+import com.autonomousapps.model.ProjectAdvice
+
+import static com.autonomousapps.AdviceHelper.actualProjectAdvice
+import static com.autonomousapps.AdviceHelper.emptyProjectAdviceFor
+import static com.autonomousapps.kit.gradle.Dependency.project
+
+/**
+ * In this app project, there is a layout file that references a string resource by name (not fully-qualified, because
+ * XML). Two separate Android libraries provide this same resource. The project has `android.nonTransitiveRClass`
+ * enabled. Is this a problem?
+ */
+final class ResByResProject extends AbstractAndroidProject {
+
+ final GradleProject gradleProject
+ private final String agpVersion
+
+ ResByResProject(String agpVersion) {
+ super(agpVersion)
+ this.agpVersion = agpVersion
+ this.gradleProject = build()
+ }
+
+ private GradleProject build() {
+ return newAndroidGradleProjectBuilder(agpVersion)
+ .withAndroidSubproject('app') { app ->
+ app.withBuildScript { bs ->
+ bs.plugins(androidApp())
+ bs.android = defaultAndroidAppBlock(false)
+ bs.dependencies(
+ project('implementation', ':res1'),
+ project('implementation', ':res2'),
+ )
+ }
+ app.manifest = appEmpty()
+ app.layouts(
+ AndroidLayout
+ .named('string_layout.xml')
+ .withContent('''\
+
+
+
+
+ '''.stripIndent())
+ )
+ }
+ .withAndroidLibProject('res1') { lib ->
+ configureResLib(lib, 'res1')
+ }
+ .withAndroidLibProject('res2') { lib ->
+ configureResLib(lib, 'res2')
+ }
+ .write()
+ }
+
+ private void configureResLib(AndroidSubproject.Builder lib, String name) {
+ lib.withBuildScript { bs ->
+ bs.plugins(androidLib(false))
+ bs.android = defaultAndroidLibBlock(false, "com.example.$name")
+ }
+ lib.manifest = libraryManifest("com.example.$name")
+ lib.strings = new AndroidStringRes(
+ '''\
+
+ OK
+ '''.stripIndent()
+ )
+ }
+
+ Set actualBuildHealth() {
+ return actualProjectAdvice(gradleProject)
+ }
+
+ final Set expectedBuildHealth = [
+ emptyProjectAdviceFor(':app'),
+ emptyProjectAdviceFor(':res1'),
+ emptyProjectAdviceFor(':res2'),
+ ]
+}
diff --git a/testkit/gradle-testkit-support/src/main/kotlin/com/autonomousapps/kit/android/AndroidLayout.kt b/testkit/gradle-testkit-support/src/main/kotlin/com/autonomousapps/kit/android/AndroidLayout.kt
index 60f7f8e5d..037d8785f 100644
--- a/testkit/gradle-testkit-support/src/main/kotlin/com/autonomousapps/kit/android/AndroidLayout.kt
+++ b/testkit/gradle-testkit-support/src/main/kotlin/com/autonomousapps/kit/android/AndroidLayout.kt
@@ -6,5 +6,21 @@ public class AndroidLayout(
public val filename: String,
public val content: String,
) {
+
override fun toString(): String = content
+
+ public class Builder(private val name: String) {
+ public fun withContent(content: String): AndroidLayout {
+ return AndroidLayout(
+ filename = name,
+ content = content,
+ )
+ }
+ }
+
+ public companion object {
+ /** The [name] should be just the filename. The path (e.g., `src/main/res/layout/`) is assumed. */
+ @JvmStatic
+ public fun named(name: String): Builder = Builder(name)
+ }
}
diff --git a/testkit/gradle-testkit-support/src/main/kotlin/com/autonomousapps/kit/android/AndroidSubproject.kt b/testkit/gradle-testkit-support/src/main/kotlin/com/autonomousapps/kit/android/AndroidSubproject.kt
index b4a24eef9..bcebb582d 100644
--- a/testkit/gradle-testkit-support/src/main/kotlin/com/autonomousapps/kit/android/AndroidSubproject.kt
+++ b/testkit/gradle-testkit-support/src/main/kotlin/com/autonomousapps/kit/android/AndroidSubproject.kt
@@ -38,7 +38,7 @@ public class AndroidSubproject(
public var styles: AndroidStyleRes? = null
public var strings: AndroidStringRes? = null
public var colors: AndroidColorRes? = null
- public var layouts: List? = null
+ public var layouts: MutableList = mutableListOf()
public val files: MutableList = mutableListOf()
// sub-builders
@@ -63,6 +63,10 @@ public class AndroidSubproject(
}
}
+ public fun layouts(vararg layouts: AndroidLayout) {
+ this.layouts.addAll(layouts.toMutableList())
+ }
+
public fun withFile(path: String, content: String) {
withFile(File(path, content))
}