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
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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('''\
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/common_ok" />
</LinearLayout>'''.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(
'''\
<resources>
<string name="common_ok">OK</string>
</resources>'''.stripIndent()
)
}

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

final Set<ProjectAdvice> expectedBuildHealth = [
emptyProjectAdviceFor(':app'),
emptyProjectAdviceFor(':res1'),
emptyProjectAdviceFor(':res2'),
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -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/<name>`) is assumed. */
@JvmStatic
public fun named(name: String): Builder = Builder(name)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<AndroidLayout>? = null
public var layouts: MutableList<AndroidLayout> = mutableListOf()
public val files: MutableList<File> = mutableListOf()

// sub-builders
Expand All @@ -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))
}
Expand Down