diff --git a/.idea/compiler.xml b/.idea/compiler.xml
index 6347cc8d2..990acf447 100644
--- a/.idea/compiler.xml
+++ b/.idea/compiler.xml
@@ -134,9 +134,6 @@
-
-
-
diff --git a/build.gradle.kts b/build.gradle.kts
index 8165438ea..e88091894 100755
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -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
}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index ffd8142eb..8cee8f42b 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -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" }
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 5b2f573df..63d938442 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -112,6 +112,7 @@ include(
":termux:termux-shared",
":termux:termux-view",
":testing:android",
+ ":testing:common",
":testing:lsp",
":testing:tooling",
":testing:unit",
diff --git a/shared/src/main/java/com/itsaky/androidide/utils/stringExt.kt b/shared/src/main/java/com/itsaky/androidide/utils/stringExt.kt
new file mode 100644
index 000000000..d90da1235
--- /dev/null
+++ b/shared/src/main/java/com/itsaky/androidide/utils/stringExt.kt
@@ -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 .
+ */
+
+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()
+ }
+}
\ No newline at end of file
diff --git a/subprojects/projects/src/test/java/com/itsaky/androidide/projects/api/ModuleProjectTest.kt b/subprojects/projects/src/test/java/com/itsaky/androidide/projects/api/ModuleProjectTest.kt
index d81e825aa..c7f779674 100644
--- a/subprojects/projects/src/test/java/com/itsaky/androidide/projects/api/ModuleProjectTest.kt
+++ b/subprojects/projects/src/test/java/com/itsaky/androidide/projects/api/ModuleProjectTest.kt
@@ -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
diff --git a/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/internal/AndroidProjectImpl.kt b/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/internal/AndroidProjectImpl.kt
index 739a954ac..964059867 100644
--- a/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/internal/AndroidProjectImpl.kt
+++ b/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/internal/AndroidProjectImpl.kt
@@ -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 {
@@ -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> {
@@ -131,11 +130,8 @@ internal class AndroidProjectImpl(
}
}
- private fun fillLibrary(
- item: GraphItem,
- libraries: Map,
- seen: HashMap
- ): DefaultLibrary? {
+ private fun fillLibrary(item: GraphItem, libraries: Map,
+ seen: HashMap): 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
}
-}
\ No newline at end of file
+
+ 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()
+ 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 {
+ ""
+ }
+ }
+}
diff --git a/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/sync/AndroidProjectModelBuilder.kt b/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/sync/AndroidProjectModelBuilder.kt
index dd2eab267..4ec860e99 100644
--- a/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/sync/AndroidProjectModelBuilder.kt
+++ b/subprojects/tooling-api-impl/src/main/java/com/itsaky/androidide/tooling/impl/sync/AndroidProjectModelBuilder.kt
@@ -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
)
}
}
\ No newline at end of file
diff --git a/subprojects/tooling-api-impl/src/test/java/com/itsaky/androidide/tooling/impl/ApplicationIDComputationTest.kt b/subprojects/tooling-api-impl/src/test/java/com/itsaky/androidide/tooling/impl/ApplicationIDComputationTest.kt
new file mode 100644
index 000000000..f658ba4e2
--- /dev/null
+++ b/subprojects/tooling-api-impl/src/test/java/com/itsaky/androidide/tooling/impl/ApplicationIDComputationTest.kt
@@ -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 .
+ */
+
+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")
+ }
+ }
+}
\ No newline at end of file
diff --git a/subprojects/tooling-api-impl/src/test/java/com/itsaky/androidide/tooling/impl/CIOnlyToolingApiTests.kt b/subprojects/tooling-api-impl/src/test/java/com/itsaky/androidide/tooling/impl/CIOnlyToolingApiTests.kt
new file mode 100644
index 000000000..1481a44f4
--- /dev/null
+++ b/subprojects/tooling-api-impl/src/test/java/com/itsaky/androidide/tooling/impl/CIOnlyToolingApiTests.kt
@@ -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 .
+ */
+
+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.")
+ }
+}
\ No newline at end of file
diff --git a/subprojects/tooling-api-impl/src/test/java/com/itsaky/androidide/tooling/impl/MultiModuleAndroidProjectTest.kt b/subprojects/tooling-api-impl/src/test/java/com/itsaky/androidide/tooling/impl/MultiModuleAndroidProjectTest.kt
index 1c09276f9..40f6ca722 100644
--- a/subprojects/tooling-api-impl/src/test/java/com/itsaky/androidide/tooling/impl/MultiModuleAndroidProjectTest.kt
+++ b/subprojects/tooling-api-impl/src/test/java/com/itsaky/androidide/tooling/impl/MultiModuleAndroidProjectTest.kt
@@ -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()
- // 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> {
@@ -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()
}
diff --git a/subprojects/tooling-api-impl/src/test/java/com/itsaky/androidide/tooling/impl/common.kt b/subprojects/tooling-api-impl/src/test/java/com/itsaky/androidide/tooling/impl/common.kt
new file mode 100644
index 000000000..f483db0b9
--- /dev/null
+++ b/subprojects/tooling-api-impl/src/test/java/com/itsaky/androidide/tooling/impl/common.kt
@@ -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 .
+ */
+
+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")
+}
diff --git a/subprojects/tooling-api-model/src/main/java/com/itsaky/androidide/tooling/api/models/AndroidArtifactMetadata.kt b/subprojects/tooling-api-model/src/main/java/com/itsaky/androidide/tooling/api/models/AndroidArtifactMetadata.kt
index 29ebb7692..5a2696476 100644
--- a/subprojects/tooling-api-model/src/main/java/com/itsaky/androidide/tooling/api/models/AndroidArtifactMetadata.kt
+++ b/subprojects/tooling-api-model/src/main/java/com/itsaky/androidide/tooling/api/models/AndroidArtifactMetadata.kt
@@ -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,
diff --git a/subprojects/tooling-api/src/main/java/com/itsaky/androidide/tooling/api/messages/result/InitializeResult.kt b/subprojects/tooling-api/src/main/java/com/itsaky/androidide/tooling/api/messages/result/InitializeResult.kt
index 2a3ca7820..77efda50d 100644
--- a/subprojects/tooling-api/src/main/java/com/itsaky/androidide/tooling/api/messages/result/InitializeResult.kt
+++ b/subprojects/tooling-api/src/main/java/com/itsaky/androidide/tooling/api/messages/result/InitializeResult.kt
@@ -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)
diff --git a/testing/android/build.gradle.kts b/testing/android/build.gradle.kts
index a027fa485..5ce40b7f6 100644
--- a/testing/android/build.gradle.kts
+++ b/testing/android/build.gradle.kts
@@ -39,6 +39,8 @@ dependencies {
exclude("org.jetbrains.kotlin")
}
+ api(projects.testing.common)
+
api(projects.buildInfo)
api(projects.common)
api(projects.shared)
diff --git a/testing/common/.gitignore b/testing/common/.gitignore
new file mode 100644
index 000000000..42afabfd2
--- /dev/null
+++ b/testing/common/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/testing/common/build.gradle.kts b/testing/common/build.gradle.kts
new file mode 100644
index 000000000..715d3052c
--- /dev/null
+++ b/testing/common/build.gradle.kts
@@ -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 .
+ */
+
+plugins {
+ //noinspection JavaPluginLanguageLevel
+ id("java-library")
+ alias(libs.plugins.kotlin.jvm)
+}
+
+dependencies {
+ api(libs.tests.junit)
+}
\ No newline at end of file
diff --git a/testing/common/src/main/java/com/itsaky/androidide/testing/common/CIOnlyTestRule.kt b/testing/common/src/main/java/com/itsaky/androidide/testing/common/CIOnlyTestRule.kt
new file mode 100644
index 000000000..418c35577
--- /dev/null
+++ b/testing/common/src/main/java/com/itsaky/androidide/testing/common/CIOnlyTestRule.kt
@@ -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()
+}
\ No newline at end of file
diff --git a/testing/common/src/main/java/com/itsaky/androidide/testing/common/NoOpStatement.kt b/testing/common/src/main/java/com/itsaky/androidide/testing/common/NoOpStatement.kt
new file mode 100644
index 000000000..517fd4748
--- /dev/null
+++ b/testing/common/src/main/java/com/itsaky/androidide/testing/common/NoOpStatement.kt
@@ -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 .
+ */
+
+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() {
+ }
+}
\ No newline at end of file
diff --git a/testing/lsp/build.gradle.kts b/testing/lsp/build.gradle.kts
index 8ef7f0821..247b67941 100644
--- a/testing/lsp/build.gradle.kts
+++ b/testing/lsp/build.gradle.kts
@@ -33,6 +33,7 @@ dependencies {
api(projects.eventbusAndroid)
api(libs.common.editor)
+ api(projects.testing.common)
api(projects.testing.tooling)
api(projects.testing.unit)
}
\ No newline at end of file
diff --git a/testing/lsp/src/main/java/com/itsaky/androidide/lsp/api/LSPTest.kt b/testing/lsp/src/main/java/com/itsaky/androidide/lsp/api/LSPTest.kt
index c0eb8370e..dc2d898b0 100644
--- a/testing/lsp/src/main/java/com/itsaky/androidide/lsp/api/LSPTest.kt
+++ b/testing/lsp/src/main/java/com/itsaky/androidide/lsp/api/LSPTest.kt
@@ -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
diff --git a/testing/resources/test-project/app/build.gradle.in b/testing/resources/test-project/app/build.gradle.in
index 30c36c722..dd55a3286 100644
--- a/testing/resources/test-project/app/build.gradle.in
+++ b/testing/resources/test-project/app/build.gradle.in
@@ -34,6 +34,8 @@ android {
buildFeatures {
viewBinding true
}
+
+ @@ANDROID_BLOCK_CONFIG@@
}
task thisIsATestTask () {
diff --git a/testing/tooling/build.gradle.kts b/testing/tooling/build.gradle.kts
index 417b42a4d..2035add0c 100644
--- a/testing/tooling/build.gradle.kts
+++ b/testing/tooling/build.gradle.kts
@@ -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)
}
\ No newline at end of file
diff --git a/testing/tooling/src/main/java/com/itsaky/androidide/tooling/testing/ToolingApiTestLauncher.kt b/testing/tooling/src/main/java/com/itsaky/androidide/testing/tooling/ToolingApiTestLauncher.kt
similarity index 95%
rename from testing/tooling/src/main/java/com/itsaky/androidide/tooling/testing/ToolingApiTestLauncher.kt
rename to testing/tooling/src/main/java/com/itsaky/androidide/testing/tooling/ToolingApiTestLauncher.kt
index 20e3f278b..00ceff670 100644
--- a/testing/tooling/src/main/java/com/itsaky/androidide/tooling/testing/ToolingApiTestLauncher.kt
+++ b/testing/tooling/src/main/java/com/itsaky/androidide/testing/tooling/ToolingApiTestLauncher.kt
@@ -15,7 +15,7 @@
* along with AndroidIDE. If not, see .
*/
-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 = 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) {
diff --git a/testing/tooling/src/main/java/com/itsaky/androidide/tooling/testing/utils.kt b/testing/tooling/src/main/java/com/itsaky/androidide/testing/tooling/utils.kt
similarity index 96%
rename from testing/tooling/src/main/java/com/itsaky/androidide/tooling/testing/utils.kt
rename to testing/tooling/src/main/java/com/itsaky/androidide/testing/tooling/utils.kt
index b3811a876..68b5a7faf 100644
--- a/testing/tooling/src/main/java/com/itsaky/androidide/tooling/testing/utils.kt
+++ b/testing/tooling/src/main/java/com/itsaky/androidide/testing/tooling/utils.kt
@@ -15,7 +15,7 @@
* along with AndroidIDE. If not, see .
*/
-package com.itsaky.androidide.tooling.testing
+package com.itsaky.androidide.testing.tooling
fun findAndroidHome(): String {
var androidHome = System.getenv("ANDROID_HOME")
diff --git a/testing/unit/build.gradle.kts b/testing/unit/build.gradle.kts
index 347f1c4e2..3fb4b2be0 100644
--- a/testing/unit/build.gradle.kts
+++ b/testing/unit/build.gradle.kts
@@ -34,4 +34,6 @@ dependencies {
api(projects.buildInfo)
api(projects.common)
api(projects.shared)
+
+ api(projects.testing.common)
}
\ No newline at end of file
diff --git a/xml-inflater/src/test/java/com/itsaky/androidide/inflater/XmlInflaterTest.kt b/xml-inflater/src/test/java/com/itsaky/androidide/inflater/XmlInflaterTest.kt
index 2133c1507..311fe1959 100644
--- a/xml-inflater/src/test/java/com/itsaky/androidide/inflater/XmlInflaterTest.kt
+++ b/xml-inflater/src/test/java/com/itsaky/androidide/inflater/XmlInflaterTest.kt
@@ -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