| // Copyright 2014 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package com.flutter.gradle |
| |
| import com.android.build.api.AndroidPluginVersion |
| import com.android.build.api.variant.AndroidComponentsExtension |
| import com.android.build.api.variant.Variant |
| import com.flutter.gradle.DependencyVersionChecker.AGP_NAME |
| import com.flutter.gradle.DependencyVersionChecker.GRADLE_NAME |
| import com.flutter.gradle.DependencyVersionChecker.JAVA_NAME |
| import com.flutter.gradle.DependencyVersionChecker.KGP_NAME |
| import com.flutter.gradle.DependencyVersionChecker.MIN_SDK_NAME |
| import com.flutter.gradle.DependencyVersionChecker.OUT_OF_SUPPORT_RANGE_PROPERTY |
| import com.flutter.gradle.DependencyVersionChecker.POTENTIAL_JAVA_FIX |
| import com.flutter.gradle.DependencyVersionChecker.errorAGPVersion |
| import com.flutter.gradle.DependencyVersionChecker.errorGradleVersion |
| import com.flutter.gradle.DependencyVersionChecker.errorKGPVersion |
| import com.flutter.gradle.DependencyVersionChecker.errorMinSdkVersion |
| import com.flutter.gradle.DependencyVersionChecker.getErrorMessage |
| import com.flutter.gradle.DependencyVersionChecker.getFlavorSpecificMessage |
| import com.flutter.gradle.DependencyVersionChecker.getPotentialAGPFix |
| import com.flutter.gradle.DependencyVersionChecker.getPotentialGradleFix |
| import com.flutter.gradle.DependencyVersionChecker.getPotentialKGPFix |
| import com.flutter.gradle.DependencyVersionChecker.getPotentialSDKFix |
| import com.flutter.gradle.DependencyVersionChecker.getWarnMessage |
| import com.flutter.gradle.DependencyVersionChecker.warnAGPVersion |
| import com.flutter.gradle.DependencyVersionChecker.warnGradleVersion |
| import com.flutter.gradle.DependencyVersionChecker.warnJavaVersion |
| import com.flutter.gradle.DependencyVersionChecker.warnKGPVersion |
| import com.flutter.gradle.DependencyVersionChecker.warnMinSdkVersion |
| import io.mockk.every |
| import io.mockk.mockk |
| import io.mockk.mockkStatic |
| import io.mockk.slot |
| import io.mockk.verify |
| import org.gradle.api.Action |
| import org.gradle.api.JavaVersion |
| import org.gradle.api.Project |
| import org.gradle.api.Task |
| import org.gradle.api.logging.Logger |
| import org.gradle.api.plugins.ExtraPropertiesExtension |
| import org.gradle.api.tasks.TaskContainer |
| import org.gradle.internal.extensions.core.extra |
| import kotlin.test.Test |
| import kotlin.test.assertEquals |
| import kotlin.test.assertFailsWith |
| |
| private const val FAKE_PROJECT_ROOT_DIR = "/fake/root/dir" |
| |
| // The following values will need to be modified when the corresponding "warn$DepName" versions |
| // are updated in DependencyVersionChecker.kt |
| // These values should match the flutter create template values. |
| // In //packages/flutter_tools/lib/src/android/gradle_utils.dart |
| private const val SUPPORTED_GRADLE_VERSION: String = "8.12" |
| private val SUPPORTED_JAVA_VERSION: JavaVersion = JavaVersion.VERSION_17 |
| private val SUPPORTED_AGP_VERSION: AndroidPluginVersion = AndroidPluginVersion(8, 9, 1) |
| private const val SUPPORTED_KGP_VERSION: String = "2.1.0" |
| private val SUPPORTED_SDK_VERSION: MinSdkVersion = MinSdkVersion("release", 30) |
| |
| class DependencyVersionCheckerTest { |
| @Test |
| fun `AGP version in error range results in DependencyValidationException`() { |
| val exampleErrorAgpVersion = AndroidPluginVersion(8, 1, 0) |
| val mockProject = MockProjectFactory.createMockProjectWithSpecifiedDependencyVersions(agpVersion = exampleErrorAgpVersion) |
| |
| val mockExtraPropertiesExtension = mockProject.extra |
| every { mockExtraPropertiesExtension.set(any(), any()) } returns Unit |
| |
| val dependencyValidationException = |
| assertFailsWith<DependencyValidationException> { DependencyVersionChecker.checkDependencyVersions(mockProject) } |
| assert( |
| dependencyValidationException.message == |
| getErrorMessage( |
| AGP_NAME, |
| exampleErrorAgpVersion.toString(), |
| errorAGPVersion.toString(), |
| getPotentialAGPFix(FAKE_PROJECT_ROOT_DIR) |
| ) |
| ) |
| verify { mockExtraPropertiesExtension.set(OUT_OF_SUPPORT_RANGE_PROPERTY, true) } |
| } |
| |
| @Test |
| fun `AGP version in warn range results in warning logs`() { |
| val exampleWarnAgpVersion = AndroidPluginVersion(8, 2, 0) |
| val mockProject = MockProjectFactory.createMockProjectWithSpecifiedDependencyVersions(agpVersion = exampleWarnAgpVersion) |
| |
| val mockExtraPropertiesExtension = mockProject.extra |
| every { mockExtraPropertiesExtension.set(OUT_OF_SUPPORT_RANGE_PROPERTY, false) } returns Unit |
| val mockLogger = mockProject.logger |
| every { mockLogger.error(any()) } returns Unit |
| |
| DependencyVersionChecker.checkDependencyVersions(mockProject) |
| verify { |
| mockLogger.error( |
| getWarnMessage( |
| AGP_NAME, |
| exampleWarnAgpVersion.toString(), |
| warnAGPVersion.toString(), |
| getPotentialAGPFix(FAKE_PROJECT_ROOT_DIR) |
| ) |
| ) |
| } |
| verify(exactly = 0) { mockExtraPropertiesExtension.set(OUT_OF_SUPPORT_RANGE_PROPERTY, true) } |
| } |
| |
| @Test |
| fun `KGP version in error range results in DependencyValidationException`() { |
| val exampleErrorKgpVersion = "1.6.0" |
| val mockProject = MockProjectFactory.createMockProjectWithSpecifiedDependencyVersions(kgpVersion = exampleErrorKgpVersion) |
| |
| val mockExtraPropertiesExtension = mockProject.extra |
| every { mockExtraPropertiesExtension.set(any(), any()) } returns Unit |
| |
| val dependencyValidationException = |
| assertFailsWith<DependencyValidationException> { DependencyVersionChecker.checkDependencyVersions(mockProject) } |
| |
| println(dependencyValidationException.message) |
| assert( |
| dependencyValidationException.message == |
| getErrorMessage( |
| KGP_NAME, |
| exampleErrorKgpVersion, |
| errorKGPVersion.toString(), |
| getPotentialKGPFix(FAKE_PROJECT_ROOT_DIR) |
| ) |
| ) |
| verify { mockExtraPropertiesExtension.set(OUT_OF_SUPPORT_RANGE_PROPERTY, true) } |
| } |
| |
| @Test |
| fun `KGP version in warn range results in warning logs`() { |
| val exampleWarnKgpVersion = "1.8.20" |
| val mockProject = MockProjectFactory.createMockProjectWithSpecifiedDependencyVersions(kgpVersion = exampleWarnKgpVersion) |
| |
| val mockExtraPropertiesExtension = mockProject.extra |
| every { mockExtraPropertiesExtension.set(OUT_OF_SUPPORT_RANGE_PROPERTY, false) } returns Unit |
| val mockLogger = mockProject.logger |
| every { mockLogger.error(any()) } returns Unit |
| |
| DependencyVersionChecker.checkDependencyVersions(mockProject) |
| verify { |
| mockLogger.error( |
| getWarnMessage( |
| KGP_NAME, |
| exampleWarnKgpVersion, |
| warnKGPVersion.toString(), |
| getPotentialKGPFix(FAKE_PROJECT_ROOT_DIR) |
| ) |
| ) |
| } |
| verify(exactly = 0) { mockExtraPropertiesExtension.set(OUT_OF_SUPPORT_RANGE_PROPERTY, true) } |
| } |
| |
| // No test for Java version in error range, as the lowest supported Java version is also the |
| // lowest possible. |
| |
| @Test |
| fun `Java version in warn range results in warning logs`() { |
| val exampleWarnJavaVersion = JavaVersion.VERSION_16 |
| val mockProject = MockProjectFactory.createMockProjectWithSpecifiedDependencyVersions(javaVersion = exampleWarnJavaVersion) |
| |
| val mockExtraPropertiesExtension = mockProject.extra |
| every { mockExtraPropertiesExtension.set(OUT_OF_SUPPORT_RANGE_PROPERTY, false) } returns Unit |
| val mockLogger = mockProject.logger |
| every { mockLogger.error(any()) } returns Unit |
| |
| DependencyVersionChecker.checkDependencyVersions(mockProject) |
| verify { |
| mockLogger.error( |
| getWarnMessage( |
| JAVA_NAME, |
| exampleWarnJavaVersion.toString(), |
| warnJavaVersion.toString(), |
| POTENTIAL_JAVA_FIX |
| ) |
| ) |
| } |
| verify(exactly = 0) { mockExtraPropertiesExtension.set(OUT_OF_SUPPORT_RANGE_PROPERTY, true) } |
| } |
| |
| @Test |
| fun `Gradle version in error range results in DependencyValidationException`() { |
| val exampleErrorGradleVersion = "7.0.0" |
| val mockProject = MockProjectFactory.createMockProjectWithSpecifiedDependencyVersions(gradleVersion = exampleErrorGradleVersion) |
| |
| val mockExtraPropertiesExtension = mockProject.extra |
| every { mockExtraPropertiesExtension.set(any(), any()) } returns Unit |
| |
| val dependencyValidationException = |
| assertFailsWith<DependencyValidationException> { DependencyVersionChecker.checkDependencyVersions(mockProject) } |
| |
| assert( |
| dependencyValidationException.message == |
| getErrorMessage( |
| GRADLE_NAME, |
| exampleErrorGradleVersion, |
| errorGradleVersion.toString(), |
| getPotentialGradleFix(FAKE_PROJECT_ROOT_DIR) |
| ) |
| ) |
| verify { mockExtraPropertiesExtension.set(OUT_OF_SUPPORT_RANGE_PROPERTY, true) } |
| } |
| |
| @Test |
| fun `Gradle version in warn range results in warning logs`() { |
| val exampleWarnGradleVersion = "8.5.0" |
| val mockProject = MockProjectFactory.createMockProjectWithSpecifiedDependencyVersions(gradleVersion = exampleWarnGradleVersion) |
| |
| val mockExtraPropertiesExtension = mockProject.extra |
| every { mockExtraPropertiesExtension.set(OUT_OF_SUPPORT_RANGE_PROPERTY, false) } returns Unit |
| val mockLogger = mockProject.logger |
| every { mockLogger.error(any()) } returns Unit |
| |
| DependencyVersionChecker.checkDependencyVersions(mockProject) |
| verify { |
| mockLogger.error( |
| getWarnMessage( |
| GRADLE_NAME, |
| exampleWarnGradleVersion, |
| warnGradleVersion.toString(), |
| getPotentialGradleFix(FAKE_PROJECT_ROOT_DIR) |
| ) |
| ) |
| } |
| verify(exactly = 0) { mockExtraPropertiesExtension.set(OUT_OF_SUPPORT_RANGE_PROPERTY, true) } |
| } |
| |
| @Test |
| fun `min SDK version in warn range results in warning logs`() { |
| val exampleWarnSDKVersion = 23 |
| val flavorName1 = "flavor1" |
| val flavorName2 = "flavor2" |
| val mockProject = |
| MockProjectFactory.createMockProjectWithSpecifiedDependencyVersions( |
| minSdkVersions = |
| listOf( |
| MinSdkVersion(flavorName1, exampleWarnSDKVersion), |
| MinSdkVersion(flavorName2, exampleWarnSDKVersion) |
| ) |
| ) |
| |
| val mockExtraPropertiesExtension = mockProject.extra |
| every { |
| mockExtraPropertiesExtension.set( |
| OUT_OF_SUPPORT_RANGE_PROPERTY, |
| false |
| ) |
| } returns Unit |
| val mockLogger = mockProject.logger |
| every { mockLogger.error(any()) } returns Unit |
| |
| DependencyVersionChecker.checkDependencyVersions(mockProject) |
| verify { |
| mockLogger.error( |
| getWarnMessage( |
| getFlavorSpecificMessage(flavorName1, MIN_SDK_NAME), |
| exampleWarnSDKVersion.toString(), |
| warnMinSdkVersion.toString(), |
| getPotentialSDKFix(FAKE_PROJECT_ROOT_DIR) |
| ) |
| ) |
| mockLogger.error( |
| getWarnMessage( |
| getFlavorSpecificMessage(flavorName2, MIN_SDK_NAME), |
| exampleWarnSDKVersion.toString(), |
| warnMinSdkVersion.toString(), |
| getPotentialSDKFix(FAKE_PROJECT_ROOT_DIR) |
| ) |
| ) |
| } |
| verify(exactly = 0) { |
| mockExtraPropertiesExtension.set( |
| OUT_OF_SUPPORT_RANGE_PROPERTY, |
| true |
| ) |
| } |
| } |
| |
| @Test |
| fun `min SDK version in error range results in DependencyValidationException`() { |
| val exampleErrorSDKVersion = 0 |
| val flavorName = "flavor1" |
| val mockProject = |
| MockProjectFactory.createMockProjectWithSpecifiedDependencyVersions( |
| minSdkVersions = |
| listOf( |
| MinSdkVersion(flavorName, exampleErrorSDKVersion) |
| ) |
| ) |
| |
| val mockExtraPropertiesExtension = mockProject.extra |
| val mockLogger = mockProject.logger |
| every { mockExtraPropertiesExtension.set(any(), any()) } returns Unit |
| every { mockLogger.error(any()) } returns Unit |
| |
| val dependencyValidationException = |
| assertFailsWith<DependencyValidationException> { |
| DependencyVersionChecker.checkDependencyVersions( |
| mockProject |
| ) |
| } |
| |
| assert( |
| dependencyValidationException.message == |
| getErrorMessage( |
| getFlavorSpecificMessage(flavorName, MIN_SDK_NAME), |
| exampleErrorSDKVersion.toString(), |
| errorMinSdkVersion.toString(), |
| getPotentialSDKFix(FAKE_PROJECT_ROOT_DIR) |
| ) |
| ) |
| verify(exactly = 1) { |
| mockExtraPropertiesExtension.set( |
| OUT_OF_SUPPORT_RANGE_PROPERTY, |
| true |
| ) |
| } |
| } |
| |
| @Test |
| fun `checkMinSdkVersion throws error when in error range for min SDK version`() { |
| val mockLogger = mockk<Logger>() |
| val mockExtraPropertiesExtension = mockk<ExtraPropertiesExtension>() |
| val projectDir = "projectDir" |
| val flavor = "flavor" |
| val version = 0 |
| |
| every { mockExtraPropertiesExtension.set(any(), any()) } returns Unit |
| every { mockLogger.error(any()) } returns Unit |
| |
| val dependencyValidationException = |
| assertFailsWith<DependencyValidationException> { |
| DependencyVersionChecker.checkMinSdkVersion( |
| minSdkVersion = MinSdkVersion(flavor, version), |
| projectDirectory = projectDir, |
| logger = mockLogger |
| ) |
| } |
| |
| assertEquals( |
| dependencyValidationException.message, |
| "Error: Your project's minimum Android SDK (flavor='flavor') version ($version) is lower than " + |
| "Flutter's minimum supported version of $errorMinSdkVersion. Please upgrade your minimum Android SDK " + |
| "(flavor='flavor') version. \n" + |
| "Alternatively, use the flag \"--android-skip-build-dependency-validation\" to " + |
| "bypass this check.\n" + |
| "\n" + |
| "Potential fix: Your project's minimum Android SDK version is typically defined in " + |
| "the android block of the app-level `build.gradle(.kts)` file " + |
| "(projectDir/app/build.gradle(.kts))." |
| ) |
| } |
| |
| @Test |
| fun `checkMinSdkVersion logs warning when in warning range for min SDK version`() { |
| val mockLogger = mockk<Logger>() |
| val mockExtraPropertiesExtension = mockk<ExtraPropertiesExtension>() |
| val projectDir = "projectDir" |
| val flavor = "flavor" |
| val version = 23 |
| |
| every { mockExtraPropertiesExtension.set(any(), any()) } returns Unit |
| every { mockLogger.error(any()) } returns Unit |
| |
| DependencyVersionChecker.checkMinSdkVersion( |
| minSdkVersion = MinSdkVersion(flavor, version), |
| projectDirectory = projectDir, |
| logger = mockLogger |
| ) |
| |
| val warningMessageSlot = slot<String>() |
| verify { |
| mockLogger.error(capture(warningMessageSlot)) |
| } |
| |
| assertEquals( |
| warningMessageSlot.captured, |
| "Warning: Flutter support for your project's minimum Android SDK (flavor='flavor') " + |
| "version ($version) will soon be dropped. Please upgrade your minimum Android SDK " + |
| "(flavor='flavor') version to a version of at least $warnMinSdkVersion soon.\n" + |
| "Alternatively, use the flag \"--android-skip-build-dependency-validation\" to " + |
| "bypass this check.\n" + |
| "\n" + |
| "Potential fix: Your project's minimum Android SDK version is typically defined in " + |
| "the android block of the app-level `build.gradle(.kts)` file " + |
| "(projectDir/app/build.gradle(.kts))." |
| ) |
| } |
| } |
| |
| // There isn't a way to create a real org.gradle.api.Project object for testing unfortunately, so |
| // these tests rely heavily on mocking. |
| // |
| // TODO(gmackall): We should consider adding functional tests built on top of a testing framework |
| // perhaps like |
| // https://github.com/autonomousapps/dependency-analysis-gradle-plugin/tree/main/testkit |
| // as a way to fill this gap in testing (combined with moving functionality to individual tasks |
| // that can be tested independently). |
| private object MockProjectFactory { |
| fun createMockProjectWithSpecifiedDependencyVersions( |
| javaVersion: JavaVersion = SUPPORTED_JAVA_VERSION, |
| gradleVersion: String = SUPPORTED_GRADLE_VERSION, |
| agpVersion: AndroidPluginVersion = SUPPORTED_AGP_VERSION, |
| kgpVersion: String = SUPPORTED_KGP_VERSION, |
| minSdkVersions: List<MinSdkVersion> = listOf(SUPPORTED_SDK_VERSION) |
| ): Project { |
| // Java |
| mockkStatic(JavaVersion::class) |
| every { JavaVersion.current() } returns javaVersion |
| |
| // Gradle |
| val mockProject = mockk<Project>() |
| every { mockProject.gradle.gradleVersion } returns gradleVersion |
| |
| // AGP |
| val mockAndroidComponentsExtension = mockk<AndroidComponentsExtension<*, *, *>>() |
| every { mockProject.extensions.findByType(AndroidComponentsExtension::class.java) } returns mockAndroidComponentsExtension |
| every { mockAndroidComponentsExtension.pluginVersion } returns agpVersion |
| |
| // KGP |
| every { mockProject.hasProperty(eq("kotlin_version")) } returns true |
| every { mockProject.properties["kotlin_version"] } returns kgpVersion |
| |
| // Logger |
| val mockLogger = mockk<Logger>() |
| every { mockProject.logger } returns mockLogger |
| |
| // Extra properties extension |
| val mockExtraPropertiesExtension = mockk<ExtraPropertiesExtension>() |
| every { mockProject.extra } returns mockExtraPropertiesExtension |
| |
| // Project path |
| every { mockProject.rootDir.path } returns FAKE_PROJECT_ROOT_DIR |
| |
| // SDK |
| val actionSlot = slot<Action<Project>>() |
| every { mockProject.afterEvaluate(capture(actionSlot)) } answers { |
| actionSlot.captured.execute(mockProject) |
| return@answers Unit |
| } |
| val onVariantsFnSlot = slot<(Variant) -> Unit>() |
| every { mockAndroidComponentsExtension.selector() } returns |
| mockk { |
| every { all() } returns mockk() |
| } |
| every { mockProject.tasks } returns |
| mockk<TaskContainer> { |
| val registerTaskSlot = slot<Action<Task>>() |
| every { register(any(), capture(registerTaskSlot)) } answers registerAnswer@{ |
| registerTaskSlot.captured.execute( |
| mockk { |
| val doLastActionSlot = slot<Action<Task>>() |
| every { doLast(capture(doLastActionSlot)) } answers doLastAnswer@{ |
| doLastActionSlot.captured.execute(mockk()) |
| return@doLastAnswer mockk() |
| } |
| } |
| ) |
| return@registerAnswer mockk() |
| } |
| |
| every { named(any<String>()) } returns |
| mockk { |
| every { configure(any<Action<Task>>()) } returns mockk() |
| } |
| } |
| every { |
| mockAndroidComponentsExtension.onVariants( |
| any(), |
| capture(onVariantsFnSlot) |
| ) |
| } answers { |
| minSdkVersions.forEach { |
| val variant = mockk<Variant>() |
| every { variant.name } returns it.flavor |
| every { variant.minSdk } returns mockk { every { apiLevel } returns it.version } |
| onVariantsFnSlot.captured.invoke(variant) |
| } |
| return@answers Unit |
| } |
| |
| return mockProject |
| } |
| } |