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