fix(tooling-api): fetch variant-specific application IDs in application projects (#1387)

This commit is contained in:
Akash Yadav
2024-01-21 20:10:56 +05:30
parent 348dfeecbb
commit adee0d678b
27 changed files with 998 additions and 313 deletions
-3
View File
@@ -134,9 +134,6 @@
<module name="AndroidIDE.templates-testing.test" target="11" />
<module name="AndroidIDE.testing-android" target="17" />
<module name="AndroidIDE.testing.android.test" target="11" />
<module name="AndroidIDE.testing.common" target="11" />
<module name="AndroidIDE.testing.common.main" target="11" />
<module name="AndroidIDE.testing.common.test" target="11" />
<module name="AndroidIDE.testing.tooling-api" target="11" />
<module name="AndroidIDE.testing.tooling-api.main" target="11" />
<module name="AndroidIDE.testing.tooling-api.test" target="11" />
+1
View File
@@ -28,6 +28,7 @@ plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.android.library) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.jvm) apply false
alias(libs.plugins.maven.publish) apply false
alias(libs.plugins.gradle.publish) apply false
}
+1
View File
@@ -148,5 +148,6 @@ maven-publish = { module = "com.vanniktech:gradle-maven-publish-plugin", version
android-application = { id = "com.android.application", version.ref = "agp" }
android-library = { id = "com.android.library", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
maven-publish = { id = "com.vanniktech.maven.publish.base", version.ref = "maven-publish-plugin" }
gradle-publish = { id = "com.gradle.plugin-publish", version = "1.2.1" }
+1
View File
@@ -112,6 +112,7 @@ include(
":termux:termux-shared",
":termux:termux-view",
":testing:android",
":testing:common",
":testing:lsp",
":testing:tooling",
":testing:unit",
@@ -0,0 +1,30 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.utils
import java.util.Locale
/**
* Capitalizes the string.
*/
@JvmOverloads
fun String.capitalizeString(locale: Locale = Locale.getDefault()): String {
return replaceFirstChar { char ->
if (char.isLowerCase()) char.titlecase(locale) else char.toString()
}
}
@@ -26,14 +26,11 @@ import com.itsaky.androidide.projects.IProjectManager
import com.itsaky.androidide.projects.ProjectManagerImpl
import com.itsaky.androidide.projects.builder.BuildService
import com.itsaky.androidide.tooling.api.IAndroidProject
import com.itsaky.androidide.tooling.api.messages.InitializeProjectParams
import com.itsaky.androidide.tooling.testing.ToolingApiTestLauncher
import com.itsaky.androidide.testing.tooling.ToolingApiTestLauncher
import com.itsaky.androidide.utils.FileProvider
import com.itsaky.androidide.utils.SourceClassTrie.SourceNode
import kotlinx.coroutines.runBlocking
import java.io.File
import java.nio.file.Files
import kotlin.io.path.pathString
import kotlin.io.path.writeText
import org.junit.Test
import org.junit.runner.RunWith
@@ -17,10 +17,13 @@
package com.itsaky.androidide.tooling.impl.internal
import com.android.builder.model.v2.dsl.BuildType
import com.android.builder.model.v2.ide.AndroidArtifact
import com.android.builder.model.v2.ide.GraphItem
import com.android.builder.model.v2.ide.Library
import com.android.builder.model.v2.ide.ProjectType
import com.android.builder.model.v2.ide.Variant
import com.android.builder.model.v2.models.AndroidDsl
import com.android.builder.model.v2.models.AndroidProject
import com.android.builder.model.v2.models.BasicAndroidProject
import com.android.builder.model.v2.models.VariantDependencies
@@ -40,9 +43,9 @@ import com.itsaky.androidide.tooling.api.models.ProjectMetadata
import com.itsaky.androidide.tooling.api.models.params.StringParameter
import com.itsaky.androidide.tooling.api.util.AndroidModulePropertyCopier
import com.itsaky.androidide.tooling.api.util.AndroidModulePropertyCopier.copy
import com.itsaky.androidide.tooling.api.util.compareSemanticVersions
import com.itsaky.androidide.tooling.api.util.extractPackageName
import com.itsaky.androidide.utils.AndroidPluginVersion
import com.itsaky.androidide.utils.capitalizeString
import org.gradle.tooling.model.GradleProject
import java.io.File
import java.io.Serializable
@@ -57,7 +60,8 @@ internal class AndroidProjectImpl(
private val basicAndroidProject: BasicAndroidProject,
private val androidProject: AndroidProject,
private val variantDependencies: VariantDependencies,
private val versions: Versions
private val versions: Versions,
private val androidDsl: AndroidDsl,
) : GradleProjectImpl(gradleProject), IAndroidProject, Serializable {
private val serialVersionUID = 1L
@@ -84,10 +88,9 @@ internal class AndroidProjectImpl(
}
}
private fun AndroidArtifact.toMetadata(name: String): AndroidArtifactMetadata {
return AndroidArtifactMetadata(
name = name,
resGenTaskName = resGenTaskName,
private fun AndroidArtifact.toMetadata(variantName: String): AndroidArtifactMetadata {
return AndroidArtifactMetadata(name = variantName,
applicationId = computeApplicationId(variantName), resGenTaskName = resGenTaskName,
assembleTaskOutputListingFile = assembleTaskOutputListingFile,
generatedResourceFolders = generatedResourceFolders,
generatedSourceFolders = generatedSourceFolders, maxSdkVersion = maxSdkVersion,
@@ -95,8 +98,7 @@ internal class AndroidProjectImpl(
sourceGenTaskName = sourceGenTaskName, assembleTaskName = assembleTaskName,
classJars = classesFolders.filter { it.name.endsWith(".jar") },
compileTaskName = compileTaskName,
targetSdkVersionOverride = targetSdkVersionOverride?.apiLevel ?: -1
)
targetSdkVersionOverride = targetSdkVersionOverride?.apiLevel ?: -1)
}
override fun getVariant(param: StringParameter): CompletableFuture<AndroidVariantMetadata?> {
@@ -106,11 +108,8 @@ internal class AndroidProjectImpl(
}
private fun Variant.toMetadata(): AndroidVariantMetadata {
return AndroidVariantMetadata(
name = name,
mainArtifact = mainArtifact.toMetadata(name),
otherArtifacts = mutableMapOf()
)
return AndroidVariantMetadata(name = name, mainArtifact = mainArtifact.toMetadata(name),
otherArtifacts = mutableMapOf())
}
override fun getBootClasspaths(): CompletableFuture<Collection<File>> {
@@ -131,11 +130,8 @@ internal class AndroidProjectImpl(
}
}
private fun fillLibrary(
item: GraphItem,
libraries: Map<String, Library>,
seen: HashMap<String, DefaultLibrary>
): DefaultLibrary? {
private fun fillLibrary(item: GraphItem, libraries: Map<String, Library>,
seen: HashMap<String, DefaultLibrary>): DefaultLibrary? {
val lib = libraries[item.key] ?: return null
val library = copy(lib)
@@ -164,8 +160,9 @@ internal class AndroidProjectImpl(
return CompletableFuture.supplyAsync {
// model sync files available only in v7.3.0 and later
return@supplyAsync if (AndroidPluginVersion.parse(versions.agp) >= AndroidPluginVersion(7, 3, 0))
copy(androidProject.modelSyncFiles)
return@supplyAsync if (AndroidPluginVersion.parse(versions.agp) >= AndroidPluginVersion(7, 3,
0)
) copy(androidProject.modelSyncFiles)
else emptyList()
}
}
@@ -191,22 +188,13 @@ internal class AndroidProjectImpl(
val gradleMetadata = super.getMetadata().get()
val viewBindingOptions = androidProject.viewBindingOptions?.let(
AndroidModulePropertyCopier::copy)
?: DefaultViewBindingOptions()
AndroidModulePropertyCopier::copy) ?: DefaultViewBindingOptions()
return@supplyAsync AndroidProjectMetadata(
gradleMetadata,
packageName,
basicAndroidProject.projectType,
copy(androidProject.flags),
copy(androidProject.javaCompileOptions),
viewBindingOptions,
androidProject.resourcePrefix,
androidProject.namespace,
androidProject.androidTestNamespace,
androidProject.testFixturesNamespace,
getClassesJar()
)
return@supplyAsync AndroidProjectMetadata(gradleMetadata, packageName,
basicAndroidProject.projectType, copy(androidProject.flags),
copy(androidProject.javaCompileOptions), viewBindingOptions, androidProject.resourcePrefix,
androidProject.namespace, androidProject.androidTestNamespace,
androidProject.testFixturesNamespace, getClassesJar())
}
}
@@ -234,4 +222,77 @@ internal class AndroidProjectImpl(
this.packageName = extractPackageName(manifestFile) ?: UNKNOWN_PACKAGE
this.shouldLookupPackage = false
}
}
private fun AndroidArtifact.computeApplicationId(variantName: String): String? {
val minAgpForAppId = AndroidPluginVersion(7, 4, 0)
return if (minAgpForAppId <= AndroidPluginVersion.parse(versions.agp)) {
applicationId
} else {
computeApplicationIdLegacy(variantName)
}
}
// Adapted from the following :
// https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-main:build-system/gradle-core/src/main/java/com/android/build/gradle/internal/core/dsl/impl/ComponentDslInfoImpl.kt;drc=6a5551bdea55c0c991f1ccf1e3f8f6f3d2cd2cb7;l=107
// https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-main:build-system/gradle-core/src/main/java/com/android/build/gradle/internal/core/dsl/impl/VariantDslInfoImpl.kt;drc=d44f5b98cd5530eceb230e0d151ad96c4277f78d;l=109
protected fun computeApplicationIdLegacy(variantName: String): String {
val basicVariant = basicAndroidProject.variants.firstOrNull { it.name == variantName }
val buildType = basicVariant?.buildType?.let { buildTypeName ->
androidDsl.buildTypes.find { buildType -> buildType.name == buildTypeName }
}!!
val appIdFromFlavor = if (basicAndroidProject.projectType == ProjectType.APPLICATION) {
androidDsl.productFlavors.find { flavor ->
"${flavor.name}${buildType.name.capitalizeString()}" == variantName
}?.applicationId
} else {
androidDsl.defaultConfig.applicationId
}
return if (appIdFromFlavor == null) {
// No appId value set from DSL; use the namespace value from the DSL.
"${androidProject.namespace}${computeApplicationIdSuffix(variantName, buildType)}"
} else {
// use value from flavors/defaultConfig
// needed to make nullability work in kotlinc
val finalAppIdFromFlavors: String = appIdFromFlavor
"$finalAppIdFromFlavors${computeApplicationIdSuffix(variantName, buildType)}"
}
}
/**
* Combines all the appId suffixes into a single one.
*
* The suffixes are separated by '.' whether their first char is a '.' or not.
*/
protected fun computeApplicationIdSuffix(variantName: String, buildType: BuildType): String {
// for the suffix we combine the suffix from all the flavors. However, we're going to
// want the higher priority one to be last.
val suffixes = mutableListOf<String>()
androidDsl.defaultConfig.applicationIdSuffix?.let {
suffixes.add(it)
}
if (basicAndroidProject.projectType == ProjectType.APPLICATION) {
val flavorSuffix = androidDsl.productFlavors.find { flavor ->
"${flavor.name}${buildType.name.capitalizeString()}" == variantName
}?.applicationIdSuffix
flavorSuffix?.also { suffixes.add(flavorSuffix) }
// then we add the build type after.
buildType.applicationIdSuffix?.also {
suffixes.add(it)
}
}
val nonEmptySuffixes = suffixes.filter { it.isNotEmpty() }
return if (nonEmptySuffixes.isNotEmpty()) {
".${nonEmptySuffixes.joinToString(separator = ".", transform = { it.removePrefix(".") })}"
} else {
""
}
}
}
@@ -16,6 +16,7 @@
*/
package com.itsaky.androidide.tooling.impl.sync
import com.android.builder.model.v2.models.AndroidDsl
import com.android.builder.model.v2.models.AndroidProject
import com.android.builder.model.v2.models.BasicAndroidProject
import com.android.builder.model.v2.models.ModelBuilderParameter
@@ -40,6 +41,7 @@ class AndroidProjectModelBuilder(initializationParams: InitializeProjectParams)
val projectPath = module.gradleProject.path
val basicModel = controller.getModelAndLog(module, BasicAndroidProject::class.java)
val androidModel = controller.getModelAndLog(module, AndroidProject::class.java)
val androidDsl = controller.getModelAndLog(module, AndroidDsl::class.java)
val variantNames = basicModel.variants.map { it.name }
log(
@@ -80,7 +82,8 @@ class AndroidProjectModelBuilder(initializationParams: InitializeProjectParams)
basicModel,
androidModel,
variantDependencies,
versions
versions,
androidDsl
)
}
}
@@ -0,0 +1,473 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.tooling.impl
import com.google.common.truth.Truth.assertThat
import com.itsaky.androidide.testing.tooling.ToolingApiTestLauncher
import com.itsaky.androidide.tooling.api.IAndroidProject
import com.itsaky.androidide.tooling.api.models.params.StringParameter
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
/**
* @author Akash Yadav
*/
@RunWith(JUnit4::class)
class ApplicationIDComputationTest {
@Test
fun `test variant-specific application ID computation for default applicationId`() {
// no custom applicationIdSuffix or applicationId is set
val (_, project, initializeResult) = ToolingApiTestLauncher().launchServer()
assertThat(initializeResult).isNotNull()
assertThat(initializeResult?.isSuccessful).isTrue()
assertThat(initializeResult?.failure).isNull()
assertThat(project).isNotNull()
val selectionResult = project.selectProject(StringParameter(":app")).get()
assertThat(selectionResult).isNotNull()
assertThat(selectionResult.isSuccessful).isTrue()
val app = project.asAndroidProject()
assertThat(app).isNotNull()
assertThat(app).isInstanceOf(IAndroidProject::class.java)
val variants = app.getVariants().get()
assertThat(variants).isNotNull()
assertThat(variants).isNotEmpty()
assertThat(variants).hasSize(2)
assertThat(variants.map { it.name }).containsExactly("debug", "release")
variants.forEach { variant ->
val applicationId = variant.mainArtifact.applicationId
assertThat(applicationId).isNotNull()
assertThat(applicationId?.trim()).isNotEmpty()
assertThat(applicationId).isEqualTo("com.itsaky.test.app")
}
}
@Test
fun `test variant-specific application ID computation for product flavors with applicationIdSuffix`() {
// only the flavors have applicationIdSuffix set, not the build types
val androidBlockConfig = """
flavorDimensions.add("default")
productFlavors {
create("full") {
dimension = "default"
applicationIdSuffix = ".full"
}
create("demo") {
dimension = "default"
applicationIdSuffix = ".demo"
}
}
""".trimIndent()
val client = ToolingApiTestLauncher.MultiVersionTestClient(
androidBlockConfig = androidBlockConfig)
val (_, project, initializeResult) = ToolingApiTestLauncher().launchServer(client = client)
assertThat(initializeResult).isNotNull()
assertThat(initializeResult?.isSuccessful).isTrue()
assertThat(initializeResult?.failure).isNull()
assertThat(project).isNotNull()
val selectionResult = project.selectProject(StringParameter(":app")).get()
assertThat(selectionResult).isNotNull()
assertThat(selectionResult.isSuccessful).isTrue()
val app = project.asAndroidProject()
assertThat(app).isNotNull()
assertThat(app).isInstanceOf(IAndroidProject::class.java)
val variants = app.getVariants().get()
assertThat(variants).isNotNull()
assertThat(variants).isNotEmpty()
assertThat(variants).hasSize(4)
assertThat(variants.map { it.name }).containsExactly("fullDebug", "fullRelease", "demoDebug",
"demoRelease")
variants.forEach { variant ->
val applicationId = variant.mainArtifact.applicationId
assertThat(applicationId).isNotNull()
assertThat(applicationId?.trim()).isNotEmpty()
assertThat(applicationId).isAnyOf("com.itsaky.test.app.full", "com.itsaky.test.app.demo")
}
}
@Test
fun `test variant-specific application ID computation for product flavors with applicationIdSuffix and multiple build types`() {
// only the flavors have applicationIdSuffix set, but additional build types have been created
val androidBlockConfig = """
flavorDimensions.add("default")
productFlavors {
create("full") {
dimension = "default"
applicationIdSuffix = ".full"
}
create("demo") {
dimension = "default"
applicationIdSuffix = ".demo"
}
}
buildTypes {
create("staging") {}
create("experimental") {}
}
""".trimIndent()
val client = ToolingApiTestLauncher.MultiVersionTestClient(
androidBlockConfig = androidBlockConfig)
val (_, project, initializeResult) = ToolingApiTestLauncher().launchServer(client = client)
assertThat(initializeResult).isNotNull()
assertThat(initializeResult?.isSuccessful).isTrue()
assertThat(initializeResult?.failure).isNull()
assertThat(project).isNotNull()
val selectionResult = project.selectProject(StringParameter(":app")).get()
assertThat(selectionResult).isNotNull()
assertThat(selectionResult.isSuccessful).isTrue()
val app = project.asAndroidProject()
assertThat(app).isNotNull()
assertThat(app).isInstanceOf(IAndroidProject::class.java)
val variants = app.getVariants().get()
assertThat(variants).isNotNull()
assertThat(variants).isNotEmpty()
assertThat(variants).hasSize(8)
assertThat(variants.map { it.name }).containsExactly("fullDebug", "fullRelease", "fullStaging",
"fullExperimental", "demoDebug", "demoRelease", "demoStaging", "demoExperimental")
variants.forEach { variant ->
val applicationId = variant.mainArtifact.applicationId
assertThat(applicationId).isNotNull()
assertThat(applicationId?.trim()).isNotEmpty()
// the build types don't have any applicationIdSuffix set
// so only suffix from the flavors should be applied here
assertThat(applicationId).isAnyOf("com.itsaky.test.app.full", "com.itsaky.test.app.demo")
}
}
@Test
fun `test variant-specific application ID computation for build types with applicationIdSuffix`() {
// only the build types have applicationIdSuffix set, not the product flavors
val androidBlockConfig = """
buildTypes {
create("staging") {
applicationIdSuffix = ".staging"
}
create("experimental") {
applicationIdSuffix = ".experimental"
}
}
""".trimIndent()
val client = ToolingApiTestLauncher.MultiVersionTestClient(
androidBlockConfig = androidBlockConfig)
val (_, project, initializeResult) = ToolingApiTestLauncher().launchServer(client = client)
assertThat(initializeResult).isNotNull()
assertThat(initializeResult?.isSuccessful).isTrue()
assertThat(initializeResult?.failure).isNull()
assertThat(project).isNotNull()
val selectionResult = project.selectProject(StringParameter(":app")).get()
assertThat(selectionResult).isNotNull()
assertThat(selectionResult.isSuccessful).isTrue()
val app = project.asAndroidProject()
assertThat(app).isNotNull()
assertThat(app).isInstanceOf(IAndroidProject::class.java)
val variants = app.getVariants().get()
assertThat(variants).isNotNull()
assertThat(variants).isNotEmpty()
assertThat(variants).hasSize(4)
assertThat(variants.map { it.name }).containsExactly("debug", "release", "staging",
"experimental")
variants.forEach { variant ->
val applicationId = variant.mainArtifact.applicationId
assertThat(applicationId).isNotNull()
assertThat(applicationId?.trim()).isNotEmpty()
assertThat(applicationId).isAnyOf("com.itsaky.test.app", "com.itsaky.test.app.staging",
"com.itsaky.test.app.experimental")
}
}
@Test
fun `test variant-specific application ID computation for build types with applicationIdSuffix and multiple product flavors`() {
// only the flavors have applicationIdSuffix set, but additional build types have been created
val androidBlockConfig = """
flavorDimensions.add("default")
productFlavors {
create("full") {
dimension = "default"
}
create("demo") {
dimension = "default"
}
}
buildTypes {
create("staging") {
applicationIdSuffix = ".staging"
}
create("experimental") {
applicationIdSuffix = ".experimental"
}
}
""".trimIndent()
val client = ToolingApiTestLauncher.MultiVersionTestClient(
androidBlockConfig = androidBlockConfig)
val (_, project, initializeResult) = ToolingApiTestLauncher().launchServer(client = client)
assertThat(initializeResult).isNotNull()
assertThat(initializeResult?.isSuccessful).isTrue()
assertThat(initializeResult?.failure).isNull()
assertThat(project).isNotNull()
val selectionResult = project.selectProject(StringParameter(":app")).get()
assertThat(selectionResult).isNotNull()
assertThat(selectionResult.isSuccessful).isTrue()
val app = project.asAndroidProject()
assertThat(app).isNotNull()
assertThat(app).isInstanceOf(IAndroidProject::class.java)
val variants = app.getVariants().get()
assertThat(variants).isNotNull()
assertThat(variants).isNotEmpty()
assertThat(variants).hasSize(8)
assertThat(variants.map { it.name }).containsExactly("fullDebug", "fullRelease", "fullStaging",
"fullExperimental", "demoDebug", "demoRelease", "demoStaging", "demoExperimental")
variants.forEach { variant ->
val applicationId = variant.mainArtifact.applicationId
assertThat(applicationId).isNotNull()
assertThat(applicationId?.trim()).isNotEmpty()
// the flavors don't have any applicationIdSuffix set
// so only suffix from the build types should be applied here
assertThat(applicationId).isAnyOf("com.itsaky.test.app", "com.itsaky.test.app.staging",
"com.itsaky.test.app.experimental")
}
}
@Test
fun `test variant-specific application ID computation for build types and product flavors with applicationIdSuffix`() {
// only the flavors have applicationIdSuffix set, but additional build types have been created
val androidBlockConfig = """
flavorDimensions.add("default")
productFlavors {
create("full") {
dimension = "default"
applicationIdSuffix = ".full"
}
create("demo") {
dimension = "default"
applicationIdSuffix = ".demo"
}
}
buildTypes {
create("staging") {
applicationIdSuffix = ".staging"
}
create("experimental") {
applicationIdSuffix = ".experimental"
}
}
""".trimIndent()
val client = ToolingApiTestLauncher.MultiVersionTestClient(
androidBlockConfig = androidBlockConfig)
val (_, project, initializeResult) = ToolingApiTestLauncher().launchServer(client = client)
assertThat(initializeResult).isNotNull()
assertThat(initializeResult?.isSuccessful).isTrue()
assertThat(initializeResult?.failure).isNull()
assertThat(project).isNotNull()
val selectionResult = project.selectProject(StringParameter(":app")).get()
assertThat(selectionResult).isNotNull()
assertThat(selectionResult.isSuccessful).isTrue()
val app = project.asAndroidProject()
assertThat(app).isNotNull()
assertThat(app).isInstanceOf(IAndroidProject::class.java)
val variants = app.getVariants().get()
assertThat(variants).isNotNull()
assertThat(variants).isNotEmpty()
assertThat(variants).hasSize(8)
assertThat(variants.map { it.name }).containsExactly("fullDebug", "fullRelease", "fullStaging",
"fullExperimental", "demoDebug", "demoRelease", "demoStaging", "demoExperimental")
variants.forEach { variant ->
val applicationId = variant.mainArtifact.applicationId
assertThat(applicationId).isNotNull()
assertThat(applicationId?.trim()).isNotEmpty()
// the flavors don't have any applicationIdSuffix set
// so only suffix from the build types should be applied here
assertThat(applicationId).isAnyOf(
// 'debug' and 'release' build types
"com.itsaky.test.app.full", "com.itsaky.test.app.demo",
// custom build types with applicationIdSuffix
"com.itsaky.test.app.full.staging", "com.itsaky.test.app.demo.staging",
"com.itsaky.test.app.full.experimental", "com.itsaky.test.app.demo.experimental")
}
}
@Test
fun `test variant-specific application ID computation for product flavors with custom applicationId`() {
// only the flavors have applicationIdSuffix set, not the build types
val androidBlockConfig = """
flavorDimensions.add("default")
productFlavors {
create("full") {
dimension = "default"
applicationId = "com.androidide.app.full"
}
create("demo") {
dimension = "default"
applicationId = "com.androidide.app.demo"
}
}
""".trimIndent()
val client = ToolingApiTestLauncher.MultiVersionTestClient(
androidBlockConfig = androidBlockConfig)
val (_, project, initializeResult) = ToolingApiTestLauncher().launchServer(client = client)
assertThat(initializeResult).isNotNull()
assertThat(initializeResult?.isSuccessful).isTrue()
assertThat(initializeResult?.failure).isNull()
assertThat(project).isNotNull()
val selectionResult = project.selectProject(StringParameter(":app")).get()
assertThat(selectionResult).isNotNull()
assertThat(selectionResult.isSuccessful).isTrue()
val app = project.asAndroidProject()
assertThat(app).isNotNull()
assertThat(app).isInstanceOf(IAndroidProject::class.java)
val variants = app.getVariants().get()
assertThat(variants).isNotNull()
assertThat(variants).isNotEmpty()
assertThat(variants).hasSize(4)
assertThat(variants.map { it.name }).containsExactly("fullDebug", "fullRelease", "demoDebug",
"demoRelease")
variants.forEach { variant ->
val applicationId = variant.mainArtifact.applicationId
assertThat(applicationId).isNotNull()
assertThat(applicationId?.trim()).isNotEmpty()
assertThat(applicationId).isAnyOf("com.androidide.app.full", "com.androidide.app.demo")
}
}
@Test
fun `test variant-specific application ID computation for product flavors with custom applicationId and applicationIdSuffix`() {
// only the flavors have applicationIdSuffix set, not the build types
val androidBlockConfig = """
flavorDimensions.add("default")
productFlavors {
create("full") {
dimension = "default"
applicationId = "com.androidide.app.full"
}
create("demo") {
dimension = "default"
applicationIdSuffix = ".demo"
}
}
""".trimIndent()
val client = ToolingApiTestLauncher.MultiVersionTestClient(
androidBlockConfig = androidBlockConfig)
val (_, project, initializeResult) = ToolingApiTestLauncher().launchServer(client = client)
assertThat(initializeResult).isNotNull()
assertThat(initializeResult?.isSuccessful).isTrue()
assertThat(initializeResult?.failure).isNull()
assertThat(project).isNotNull()
val selectionResult = project.selectProject(StringParameter(":app")).get()
assertThat(selectionResult).isNotNull()
assertThat(selectionResult.isSuccessful).isTrue()
val app = project.asAndroidProject()
assertThat(app).isNotNull()
assertThat(app).isInstanceOf(IAndroidProject::class.java)
val variants = app.getVariants().get()
assertThat(variants).isNotNull()
assertThat(variants).isNotEmpty()
assertThat(variants).hasSize(4)
assertThat(variants.map { it.name }).containsExactly("fullDebug", "fullRelease", "demoDebug",
"demoRelease")
variants.forEach { variant ->
val applicationId = variant.mainArtifact.applicationId
assertThat(applicationId).isNotNull()
assertThat(applicationId?.trim()).isNotEmpty()
assertThat(applicationId).isAnyOf("com.androidide.app.full", "com.itsaky.test.app.demo")
}
}
}
@@ -0,0 +1,111 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.tooling.impl
import com.google.common.truth.Truth.assertThat
import com.itsaky.androidide.builder.model.IDESyncIssue
import com.itsaky.androidide.buildinfo.BuildInfo
import com.itsaky.androidide.testing.common.CIOnlyTestRule
import com.itsaky.androidide.testing.tooling.ToolingApiTestLauncher
import com.itsaky.androidide.tooling.api.util.ToolingProps
import com.itsaky.androidide.utils.AndroidPluginVersion
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
/**
* @author Akash Yadav
*/
@RunWith(JUnit4::class)
class CIOnlyToolingApiTests {
@JvmField
@Rule
val ciOnlyTestRule = CIOnlyTestRule()
/**
* Tests the functionality of the tooling API implementation against multiple versions of the
* Android Gradle Plugin. This test runs only in the CI environment.
*/
@Test
fun `test CI-only simple multi module project initialization with multiple AGP versions`() {
// Test the minimum supported and the latest AGP version
val versions =
listOf(
// AGP to Gradle
BuildInfo.AGP_VERSION_MININUM to "7.3.3",
BuildInfo.AGP_VERSION_LATEST to "8.2"
)
val client = ToolingApiTestLauncher.MultiVersionTestClient()
for ((agpVersion, gradleVersion) in versions) {
client.agpVersion = agpVersion
client.gradleVersion = gradleVersion
val (server, project, result) = ToolingApiTestLauncher().launchServer(client = client)
assertThat(result?.isSuccessful).isTrue()
performBasicProjectAssertions(project = project, server = server)
}
}
@Test
fun `test CI-only latest tested AGP version warning`() {
val log = CollectingLogger()
val agpVersion = AndroidPluginVersion.parse(BuildInfo.AGP_VERSION_LATEST)
val client = ToolingApiTestLauncher.MultiVersionTestClient(
agpVersion = agpVersion.toStringSimple(),
gradleVersion = "${agpVersion.major}.${agpVersion.minor}", log = log)
val (_, project, result) = ToolingApiTestLauncher().launchServer(
client = client,
log = log,
sysProps = mapOf(
ToolingProps.TESTING_LATEST_AGP_VERSION to ToolingApiTestLauncher.MultiVersionTestClient.DEFAULT_AGP_VERSION)
)
val output = log.toString()
if (result?.isSuccessful != true) {
// print the output if the initialization fails
println(output)
}
assertThat(result?.isSuccessful).isTrue()
val syncIssues = project.getProjectSyncIssues().get()
assertThat(syncIssues).isNotNull()
assertThat(syncIssues.syncIssues).isNotEmpty()
assertThat(
syncIssues.syncIssues.find { it.type == IDESyncIssue.TYPE_AGP_VERSION_TOO_NEW }
).isNotNull()
}
@Test
fun `test CI-only minimum AGP version failure`() {
val log = CollectingLogger()
val agpVersion = "7.1.0"
val client = ToolingApiTestLauncher.MultiVersionTestClient(agpVersion = agpVersion,
gradleVersion = "7.2", log = log)
val (_, _, result) = ToolingApiTestLauncher().launchServer(client = client, log = log)
assertThat(result?.isSuccessful).isFalse()
val output = log.toString()
assertThat(output).contains(
"Android Gradle Plugin version $agpVersion is not supported by AndroidIDE.")
}
}
@@ -17,29 +17,13 @@
package com.itsaky.androidide.tooling.impl
import com.android.builder.model.v2.ide.LibraryType
import com.android.builder.model.v2.ide.ProjectType.APPLICATION
import com.google.common.truth.Truth.assertThat
import com.itsaky.androidide.builder.model.IDESyncIssue
import com.itsaky.androidide.buildinfo.BuildInfo
import com.itsaky.androidide.tooling.api.IAndroidProject
import com.itsaky.androidide.tooling.api.IJavaProject
import com.itsaky.androidide.tooling.api.IProject
import com.itsaky.androidide.tooling.api.IToolingApiServer
import com.itsaky.androidide.tooling.api.ProjectType
import com.itsaky.androidide.testing.tooling.ToolingApiTestLauncher
import com.itsaky.androidide.testing.tooling.ToolingApiTestLauncher.MultiVersionTestClient
import com.itsaky.androidide.tooling.api.messages.TaskExecutionMessage
import com.itsaky.androidide.tooling.api.messages.result.TaskExecutionResult
import com.itsaky.androidide.tooling.api.models.AndroidProjectMetadata
import com.itsaky.androidide.tooling.api.models.JavaModuleExternalDependency
import com.itsaky.androidide.tooling.api.models.JavaModuleProjectDependency
import com.itsaky.androidide.tooling.api.models.JavaProjectMetadata
import com.itsaky.androidide.tooling.api.models.params.StringParameter
import com.itsaky.androidide.tooling.events.ProgressEvent
import com.itsaky.androidide.tooling.events.task.TaskStartEvent
import com.itsaky.androidide.tooling.api.util.ToolingProps
import com.itsaky.androidide.tooling.testing.ToolingApiTestLauncher
import com.itsaky.androidide.tooling.testing.ToolingApiTestLauncher.MultiVersionTestClient
import com.itsaky.androidide.utils.AndroidPluginVersion
import com.itsaky.androidide.utils.FileProvider
import com.itsaky.androidide.utils.ILogger
import org.junit.Test
@@ -60,7 +44,7 @@ class MultiModuleAndroidProjectTest {
fun `test simple multi module project initialization`() {
val (server, project, result) = ToolingApiTestLauncher().launchServer()
assertThat(result?.isSuccessful).isTrue()
doAssertions(project, server)
performBasicProjectAssertions(project, server)
}
@Test
@@ -91,7 +75,7 @@ class MultiModuleAndroidProjectTest {
val taskPaths = mutableListOf<String>()
// do not add unresolved dependency so that teh configuration cache is properly created
// do not add unresolved dependency so that the configuration cache is properly created
val client = object : MultiVersionTestClient(excludeUnresolvedDependency = true) {
override fun getBuildArguments(): CompletableFuture<List<String>> {
@@ -159,242 +143,4 @@ class MultiModuleAndroidProjectTest {
assertThat(initResult!!.isSuccessful).isFalse()
assertThat(initResult.failure).isEqualTo(TaskExecutionResult.Failure.BUILD_CANCELLED)
}
private fun doAssertions(project: IProject, server: IToolingApiServer) {
assertThat(project).isNotNull()
// As the returned project is just a proxy,
// project instanceOf IdeGradleProject will always return false
val isInitialized = server.isServerInitialized().get()
assertThat(isInitialized).isTrue()
var selectionResult = project.selectProject(StringParameter("")).get()
assertThat(selectionResult.isSuccessful).isTrue()
assertThat(project.getType().get()).isEqualTo(ProjectType.Gradle)
selectionResult = project.selectProject(StringParameter(":app")).get()
assertThat(selectionResult).isNotNull()
assertThat(selectionResult.isSuccessful).isTrue()
val app = project.asAndroidProject()
assertThat(app).isNotNull()
assertThat(app).isInstanceOf(IAndroidProject::class.java)
var metadata = app.getMetadata().get() as AndroidProjectMetadata
assertThat(metadata).isNotNull()
assertThat(metadata.javaCompileOptions.sourceCompatibility).isEqualTo("11")
assertThat(metadata.javaCompileOptions.targetCompatibility).isEqualTo("11")
assertThat(metadata.javaCompileOptions.javaSourceVersion).isEqualTo("11")
assertThat(metadata.javaCompileOptions.javaBytecodeVersion).isEqualTo("11")
assertThat(metadata.javaCompileOptions.isCoreLibraryDesugaringEnabled).isFalse()
assertThat(metadata.androidType).isEqualTo(APPLICATION)
assertThat(metadata.packageName).isEqualTo("com.itsaky.test.app")
assertThat(metadata.viewBindingOptions).isNotNull()
assertThat(metadata.viewBindingOptions.isEnabled).isTrue()
// There are always more than 100 tasks in an android module
// Also, the tasks must contain the user defined tasks
val tasks = app.getTasks().get()
assertThat(tasks.size).isAtLeast(100)
assertThat(tasks.first { it.path == "${metadata.projectPath}:thisIsATestTask" }).isNotNull()
var libraries = app.getLibraryMap().get()
// At least one project library
assertThat(libraries.values.filter { it.type == LibraryType.PROJECT }).isNotEmpty()
// :app module includes :java-library as a dependency. But it is not transitive
assertThat(
libraries.values.firstOrNull {
it.type == LibraryType.PROJECT &&
it.projectInfo!!.projectPath == ":another-java-library" &&
it.projectInfo!!.attributes["org.gradle.usage"] == "java-api"
}
)
.isNull()
selectionResult = project.selectProject(StringParameter(":android-library")).get()
assertThat(selectionResult.isSuccessful).isTrue()
val androidLib = project.asAndroidProject()
assertThat(androidLib).isNotNull()
assertThat(androidLib).isInstanceOf(IAndroidProject::class.java)
// Make sure that transitive dependencies are included here because :android-library includes
// :java-library project which further includes :another-java-libraries project with 'api'
// configuration
libraries = androidLib.getLibraryMap().get()!!
assertThat(
libraries.values.firstOrNull {
it.type == LibraryType.PROJECT &&
it.projectInfo!!.projectPath == ":another-java-library" &&
it.projectInfo!!.attributes["org.gradle.usage"] == "java-api"
}
)
.isNotNull()
metadata = androidLib.getMetadata().get() as AndroidProjectMetadata
assertThat(metadata.javaCompileOptions.javaSourceVersion).isEqualTo("11")
assertThat(metadata.javaCompileOptions.javaBytecodeVersion).isEqualTo("11")
selectionResult = project.selectProject(StringParameter(":java-library")).get()
assertThat(selectionResult.isSuccessful).isTrue()
val javaLibrary = project.asJavaProject()
assertThat(javaLibrary).isNotNull()
assertThat(javaLibrary).isInstanceOf(IJavaProject::class.java)
var javaMetadata = javaLibrary.getMetadata().get() as JavaProjectMetadata
assertThat(javaMetadata.compilerSettings.javaSourceVersion).isEqualTo("11")
assertThat(javaMetadata.compilerSettings.javaBytecodeVersion).isEqualTo("11")
val javaDependencies = javaLibrary.getDependencies().get()
assertThat(
javaDependencies.firstOrNull {
it is JavaModuleExternalDependency &&
it.gradleArtifact != null &&
it.run {
gradleArtifact!!.group == "io.github.itsaky" &&
gradleArtifact!!.name == "nb-javac-android" &&
gradleArtifact!!.version == "17.0.0.0"
}
}
)
.isNotNull()
assertThat(
javaDependencies.firstOrNull {
it is JavaModuleProjectDependency && it.moduleName == "another-java-library"
}
)
.isNotNull()
// In case we have multiple dependencies with same name but different path
val nested =
javaDependencies
.filterIsInstance(JavaModuleProjectDependency::class.java)
.filter { it.moduleName.endsWith("nested-java-library") }
assertThat(nested).hasSize(2)
assertThat(nested[0].projectPath).isNotEqualTo(nested[1].projectPath)
assertThat(
project.selectProject(StringParameter(":does-not-exist")).get().isSuccessful).isFalse()
selectionResult = project.selectProject(StringParameter(":another-java-library")).get()
assertThat(selectionResult.isSuccessful).isTrue()
val anotherJavaLib = project.asJavaProject()
assertThat(anotherJavaLib).isNotNull()
assertThat(anotherJavaLib).isInstanceOf(IJavaProject::class.java)
javaMetadata = anotherJavaLib.getMetadata().get() as JavaProjectMetadata
assertThat(javaMetadata.compilerSettings.javaSourceVersion).isEqualTo("1.8")
assertThat(javaMetadata.compilerSettings.javaBytecodeVersion).isEqualTo("1.8")
}
/**
* Tests the functionality of the tooling API implementation against multiple versions of the
* Android Gradle Plugin. This test runs only in the CI environment.
*/
@Test
@Throws(CIOnlyException::class)
fun `test CI-only simple multi module project initialization with multiple AGP versions`() {
ciOnlyTest {
// Test the minimum supported and the latest AGP version
val versions =
listOf(
// AGP to Gradle
BuildInfo.AGP_VERSION_MININUM to "7.3.3",
BuildInfo.AGP_VERSION_LATEST to "8.2"
)
val client = MultiVersionTestClient()
for ((agpVersion, gradleVersion) in versions) {
client.agpVersion = agpVersion
client.gradleVersion = gradleVersion
val (server, project, result) = ToolingApiTestLauncher().launchServer(client = client)
assertThat(result?.isSuccessful).isTrue()
doAssertions(project = project, server = server)
}
}
}
@Test
@Throws(CIOnlyException::class)
fun `test CI-only latest tested AGP version warning`() {
ciOnlyTest {
val log = CollectingLogger()
val agpVersion = AndroidPluginVersion.parse(BuildInfo.AGP_VERSION_LATEST)
val client = MultiVersionTestClient(agpVersion = agpVersion.toStringSimple(),
gradleVersion = "${agpVersion.major}.${agpVersion.minor}", log = log)
val (_, project, result) = ToolingApiTestLauncher().launchServer(
client = client,
log = log,
sysProps = mapOf(
ToolingProps.TESTING_LATEST_AGP_VERSION to MultiVersionTestClient.DEFAULT_AGP_VERSION)
)
val output = log.toString()
if (result?.isSuccessful != true) {
// print the output if the initialization fails
println(output)
}
assertThat(result?.isSuccessful).isTrue()
val syncIssues = project.getProjectSyncIssues().get()
assertThat(syncIssues).isNotNull()
assertThat(syncIssues.syncIssues).isNotEmpty()
assertThat(
syncIssues.syncIssues.find { it.type == IDESyncIssue.TYPE_AGP_VERSION_TOO_NEW }
).isNotNull()
}
}
@Test
@Throws(CIOnlyException::class)
fun `test CI-only minimum AGP version failure`() {
ciOnlyTest {
val log = CollectingLogger()
val agpVersion = "7.1.0"
val client = MultiVersionTestClient(agpVersion = agpVersion, gradleVersion = "7.2", log = log)
val (_, _, result) = ToolingApiTestLauncher().launchServer(client = client, log = log)
assertThat(result?.isSuccessful).isFalse()
val output = log.toString()
assertThat(output).contains(
"Android Gradle Plugin version $agpVersion is not supported by AndroidIDE.")
}
}
private fun ciOnlyTest(test: () -> Unit) {
try {
assertIsCI()
test()
} catch (err: CIOnlyException) {
if (shouldTestMultipleVersions()) {
throw err
}
}
}
private fun assertIsCI() {
if (!shouldTestMultipleVersions()) {
throw CIOnlyException()
}
}
private fun shouldTestMultipleVersions(): Boolean {
return System.getenv("TEST_TOOLING_API_IMPL").let { it == "true" }
}
internal class CollectingLogger : ILogger(CollectingLogger::class.simpleName) {
private val string = StringBuilder()
override fun doLog(level: Level?, message: String?) {
string.append("${message?.trim()}${System.lineSeparator()}")
}
override fun toString(): String {
return string.toString()
}
}
private class CIOnlyException : IllegalStateException()
}
@@ -0,0 +1,172 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.tooling.impl
import com.android.builder.model.v2.ide.LibraryType
import com.google.common.truth.Truth.assertThat
import com.itsaky.androidide.tooling.api.IAndroidProject
import com.itsaky.androidide.tooling.api.IJavaProject
import com.itsaky.androidide.tooling.api.IProject
import com.itsaky.androidide.tooling.api.IToolingApiServer
import com.itsaky.androidide.tooling.api.ProjectType
import com.itsaky.androidide.tooling.api.models.AndroidProjectMetadata
import com.itsaky.androidide.tooling.api.models.JavaModuleExternalDependency
import com.itsaky.androidide.tooling.api.models.JavaModuleProjectDependency
import com.itsaky.androidide.tooling.api.models.JavaProjectMetadata
import com.itsaky.androidide.tooling.api.models.params.StringParameter
import com.itsaky.androidide.utils.ILogger
internal class CollectingLogger : ILogger(CollectingLogger::class.simpleName) {
private val string = StringBuilder()
override fun doLog(level: Level?, message: String?) {
string.append("${message?.trim()}${System.lineSeparator()}")
}
override fun toString(): String {
return string.toString()
}
}
internal fun performBasicProjectAssertions(project: IProject, server: IToolingApiServer) {
assertThat(project).isNotNull()
// As the returned project is just a proxy,
// project instanceOf IdeGradleProject will always return false
val isInitialized = server.isServerInitialized().get()
assertThat(isInitialized).isTrue()
var selectionResult = project.selectProject(StringParameter("")).get()
assertThat(selectionResult.isSuccessful).isTrue()
assertThat(project.getType().get()).isEqualTo(ProjectType.Gradle)
selectionResult = project.selectProject(StringParameter(":app")).get()
assertThat(selectionResult).isNotNull()
assertThat(selectionResult.isSuccessful).isTrue()
val app = project.asAndroidProject()
assertThat(app).isNotNull()
assertThat(app).isInstanceOf(IAndroidProject::class.java)
var metadata = app.getMetadata().get() as AndroidProjectMetadata
assertThat(metadata).isNotNull()
assertThat(metadata.javaCompileOptions.sourceCompatibility).isEqualTo("11")
assertThat(metadata.javaCompileOptions.targetCompatibility).isEqualTo("11")
assertThat(metadata.javaCompileOptions.javaSourceVersion).isEqualTo("11")
assertThat(metadata.javaCompileOptions.javaBytecodeVersion).isEqualTo("11")
assertThat(metadata.javaCompileOptions.isCoreLibraryDesugaringEnabled).isFalse()
assertThat(metadata.androidType)
.isEqualTo(com.android.builder.model.v2.ide.ProjectType.APPLICATION)
assertThat(metadata.packageName).isEqualTo("com.itsaky.test.app")
assertThat(metadata.viewBindingOptions).isNotNull()
assertThat(metadata.viewBindingOptions.isEnabled).isTrue()
// There are always more than 100 tasks in an android module
// Also, the tasks must contain the user defined tasks
val tasks = app.getTasks().get()
assertThat(tasks.size).isAtLeast(100)
assertThat(tasks.first { it.path == "${metadata.projectPath}:thisIsATestTask" }).isNotNull()
var libraries = app.getLibraryMap().get()
// At least one project library
assertThat(libraries.values.filter { it.type == LibraryType.PROJECT }).isNotEmpty()
// :app module includes :java-library as a dependency. But it is not transitive
assertThat(
libraries.values.firstOrNull {
it.type == LibraryType.PROJECT &&
it.projectInfo!!.projectPath == ":another-java-library" &&
it.projectInfo!!.attributes["org.gradle.usage"] == "java-api"
}
)
.isNull()
selectionResult = project.selectProject(StringParameter(":android-library")).get()
assertThat(selectionResult.isSuccessful).isTrue()
val androidLib = project.asAndroidProject()
assertThat(androidLib).isNotNull()
assertThat(androidLib).isInstanceOf(IAndroidProject::class.java)
// Make sure that transitive dependencies are included here because :android-library includes
// :java-library project which further includes :another-java-libraries project with 'api'
// configuration
libraries = androidLib.getLibraryMap().get()!!
assertThat(
libraries.values.firstOrNull {
it.type == LibraryType.PROJECT &&
it.projectInfo!!.projectPath == ":another-java-library" &&
it.projectInfo!!.attributes["org.gradle.usage"] == "java-api"
}
)
.isNotNull()
metadata = androidLib.getMetadata().get() as AndroidProjectMetadata
assertThat(metadata.javaCompileOptions.javaSourceVersion).isEqualTo("11")
assertThat(metadata.javaCompileOptions.javaBytecodeVersion).isEqualTo("11")
selectionResult = project.selectProject(StringParameter(":java-library")).get()
assertThat(selectionResult.isSuccessful).isTrue()
val javaLibrary = project.asJavaProject()
assertThat(javaLibrary).isNotNull()
assertThat(javaLibrary).isInstanceOf(IJavaProject::class.java)
var javaMetadata = javaLibrary.getMetadata().get() as JavaProjectMetadata
assertThat(javaMetadata.compilerSettings.javaSourceVersion).isEqualTo("11")
assertThat(javaMetadata.compilerSettings.javaBytecodeVersion).isEqualTo("11")
val javaDependencies = javaLibrary.getDependencies().get()
assertThat(
javaDependencies.firstOrNull {
it is JavaModuleExternalDependency &&
it.gradleArtifact != null &&
it.run {
gradleArtifact!!.group == "io.github.itsaky" &&
gradleArtifact!!.name == "nb-javac-android" &&
gradleArtifact!!.version == "17.0.0.0"
}
}
)
.isNotNull()
assertThat(
javaDependencies.firstOrNull {
it is JavaModuleProjectDependency && it.moduleName == "another-java-library"
}
)
.isNotNull()
// In case we have multiple dependencies with same name but different path
val nested =
javaDependencies
.filterIsInstance(JavaModuleProjectDependency::class.java)
.filter { it.moduleName.endsWith("nested-java-library") }
assertThat(nested).hasSize(2)
assertThat(nested[0].projectPath).isNotEqualTo(nested[1].projectPath)
assertThat(
project.selectProject(StringParameter(":does-not-exist")).get().isSuccessful).isFalse()
selectionResult = project.selectProject(StringParameter(":another-java-library")).get()
assertThat(selectionResult.isSuccessful).isTrue()
val anotherJavaLib = project.asJavaProject()
assertThat(anotherJavaLib).isNotNull()
assertThat(anotherJavaLib).isInstanceOf(IJavaProject::class.java)
javaMetadata = anotherJavaLib.getMetadata().get() as JavaProjectMetadata
assertThat(javaMetadata.compilerSettings.javaSourceVersion).isEqualTo("1.8")
assertThat(javaMetadata.compilerSettings.javaBytecodeVersion).isEqualTo("1.8")
}
@@ -27,6 +27,7 @@ import java.io.Serializable
*/
data class AndroidArtifactMetadata(
val name: String,
val applicationId: String?,
val resGenTaskName: String?,
val assembleTaskOutputListingFile: File?,
val generatedResourceFolders: Collection<File>,
@@ -23,4 +23,4 @@ package com.itsaky.androidide.tooling.api.messages.result
* @param isSuccessful Whether the project initialization was successful.
* @author Akash Yadav
*/
class InitializeResult(val isSuccessful: Boolean, val failure: TaskExecutionResult.Failure? = TaskExecutionResult.Failure.UNKNOWN)
class InitializeResult(val isSuccessful: Boolean, val failure: TaskExecutionResult.Failure? = null)
+2
View File
@@ -39,6 +39,8 @@ dependencies {
exclude("org.jetbrains.kotlin")
}
api(projects.testing.common)
api(projects.buildInfo)
api(projects.common)
api(projects.shared)
+1
View File
@@ -0,0 +1 @@
/build
+26
View File
@@ -0,0 +1,26 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
plugins {
//noinspection JavaPluginLanguageLevel
id("java-library")
alias(libs.plugins.kotlin.jvm)
}
dependencies {
api(libs.tests.junit)
}
@@ -0,0 +1,23 @@
package com.itsaky.androidide.testing.common
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
/**
* A [TestRule] which only runs the tests in a CI environment.
*/
class CIOnlyTestRule : TestRule {
override fun apply(base: Statement, description: Description?): Statement {
return object : Statement() {
override fun evaluate() {
if (isCi()) {
base.evaluate()
}
}
}
}
private fun isCi() = System.getenv("CI").toBoolean()
}
@@ -0,0 +1,31 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.testing.common
import org.junit.runners.model.Statement
/**
* A no-op statement.
*
* @author Akash Yadav
*/
class NoOpStatement : Statement() {
override fun evaluate() {
}
}
+1
View File
@@ -33,6 +33,7 @@ dependencies {
api(projects.eventbusAndroid)
api(libs.common.editor)
api(projects.testing.common)
api(projects.testing.tooling)
api(projects.testing.unit)
}
@@ -35,8 +35,7 @@ import com.itsaky.androidide.projects.ProjectManagerImpl
import com.itsaky.androidide.projects.builder.BuildService
import com.itsaky.androidide.tooling.api.IProject
import com.itsaky.androidide.tooling.api.IToolingApiServer
import com.itsaky.androidide.tooling.api.messages.InitializeProjectParams
import com.itsaky.androidide.tooling.testing.ToolingApiTestLauncher
import com.itsaky.androidide.testing.tooling.ToolingApiTestLauncher
import com.itsaky.androidide.utils.Environment
import com.itsaky.androidide.utils.FileProvider
import com.itsaky.androidide.utils.ILogger
@@ -34,6 +34,8 @@ android {
buildFeatures {
viewBinding true
}
@@ANDROID_BLOCK_CONFIG@@
}
task thisIsATestTask () {
+2
View File
@@ -33,6 +33,8 @@ dependencies {
api(projects.shared)
api(projects.subprojects.toolingApi)
api(projects.testing.common)
// build tooling API before tests
compileOnly(projects.subprojects.toolingApiImpl)
}
@@ -15,7 +15,7 @@
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.tooling.testing
package com.itsaky.androidide.testing.tooling
import com.itsaky.androidide.tooling.api.IProject
import com.itsaky.androidide.tooling.api.IToolingApiClient
@@ -164,6 +164,7 @@ class ToolingApiTestLauncher {
private val projectDir: Path = FileProvider.testProjectRoot(),
var agpVersion: String = DEFAULT_AGP_VERSION,
var gradleVersion: String = DEFAULT_GRADLE_VERSION,
private val androidBlockConfig: String = "",
private val log: ILogger = ILogger.newInstance(MultiVersionTestClient::class.simpleName),
private val extraArgs: List<String> = emptyList(),
private var excludeUnresolvedDependency: Boolean = false
@@ -206,11 +207,12 @@ class ToolingApiTestLauncher {
.replaceContents(dest = projectDir.resolve(buildFile),
candidate = "@@TOOLING_API_TEST_AGP_VERSION@@" to this.agpVersion)
if (!excludeUnresolvedDependency) {
projectDir.resolve(appBuildFileIn)
.replaceContents(dest = projectDir.resolve(appBuildFile),
candidate = "@@UNRESOLVED_DEPENDENCY@@" to "implementation 'unresolved:unresolved:unresolved'")
}
projectDir.resolve(appBuildFileIn)
.replaceContents(
projectDir.resolve(appBuildFile),
"//",
"@@ANDROID_BLOCK_CONFIG@@" to androidBlockConfig,
"@@UNRESOLVED_DEPENDENCY@@" to if (!excludeUnresolvedDependency) "implementation 'unresolved:unresolved:unresolved'" else "")
}
override fun onBuildSuccessful(result: BuildResult) {
@@ -15,7 +15,7 @@
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.tooling.testing
package com.itsaky.androidide.testing.tooling
fun findAndroidHome(): String {
var androidHome = System.getenv("ANDROID_HOME")
+2
View File
@@ -34,4 +34,6 @@ dependencies {
api(projects.buildInfo)
api(projects.common)
api(projects.shared)
api(projects.testing.common)
}
@@ -26,7 +26,7 @@ import com.itsaky.androidide.projects.IProjectManager
import com.itsaky.androidide.projects.api.AndroidModule
import com.itsaky.androidide.projects.builder.BuildService
import com.itsaky.androidide.projects.util.findAppModule
import com.itsaky.androidide.tooling.testing.ToolingApiTestLauncher
import com.itsaky.androidide.testing.tooling.ToolingApiTestLauncher
import kotlinx.coroutines.runBlocking
import org.junit.Ignore
import org.robolectric.Robolectric